Request Lifecycle
01
HTTP Request
Client sends GET / POST / PUT / DELETE
02
rest_api_init
Routes registered via register_rest_route()
03
Authentication
Cookie, App Password, OAuth, JWT
04
Permission Check
permission_callback fires
05
Callback
Handler runs, WP_REST_Response returned
06
JSON Response
Serialized & sent to client
Core Built-in Endpoints
| Route | Resource | Methods | Notes |
|---|---|---|---|
| /wp/v2/posts | Posts |
GET
POST
|
Supports filtering by author, category, tag, status, search |
| /wp/v2/posts/<id> | Single Post |
GET
PUT
PATCH
DELETE
|
Requires auth for PUT/PATCH/DELETE |
| /wp/v2/pages | Pages |
GET
POST
|
Hierarchical; supports parent param |
| /wp/v2/categories | Categories |
GET
POST
|
Built-in taxonomy term endpoint |
| /wp/v2/tags | Tags |
GET
POST
|
Flat taxonomy; no parent support |
| /wp/v2/users | Users |
GET
POST
|
Auth required; returns public data by default |
| /wp/v2/media | Media |
GET
POST
DELETE
|
Upload via multipart/form-data or raw binary |
| /wp/v2/comments | Comments |
GET
POST
DELETE
|
Filter by post_id, status, author |
| /wp/v2/types | Post Types |
GET
|
Lists all registered, REST-enabled post types |
| /wp/v2/settings | Site Settings |
GET
PUT
|
Admin-only; exposes site title, email, etc. |
Registering Custom Routes
register
register_rest_route()
The primary function for adding a custom REST endpoint. Always hook into
rest_api_init. Supports multiple HTTP methods per route definition.add_action( ‘rest_api_init’, function() {
register_rest_route( ‘myplugin/v1’, ‘/items’,
[
‘methods’ => ‘GET’,
‘callback’ => ‘my_get_items’,
‘permission_callback’ => ‘__return_true’,
‘args’ => [
‘status’ => [
‘default’ => ‘publish’,
‘sanitize_callback’ => ‘sanitize_text_field’,
],
],
]
);
});
register
Route with ID Parameter
Use regex in the route path to capture dynamic segments like an ID. Access them via
$request->get_param() inside the callback.register_rest_route( ‘myplugin/v1’,
‘/items/(?P<id>\d+)’,
[
‘methods’ => ‘GET’,
‘callback’ => function( $req ) {
$id = (int) $req[‘id’];
$post = get_post( $id );
if ( ! $post ) {
return new WP_Error(
‘not_found’, ‘Item not found’,
[ ‘status’ => 404 ]
);
}
return rest_ensure_response( $post );
},
‘permission_callback’ => ‘__return_true’,
‘args’ => [
‘id’ => [
‘validate_callback’ => fn($v) => is_numeric($v),
],
],
]
);
register
Multiple Methods, One Route
Pass an array of method definitions to handle GET, POST, PUT, and DELETE on the same route path cleanly.
register_rest_route( ‘myplugin/v1’, ‘/items’,
[
[
‘methods’ => WP_REST_Server::READABLE,
‘callback’ => ‘my_get_items’,
‘permission_callback’ => ‘__return_true’,
],
[
‘methods’ => WP_REST_Server::CREATABLE,
‘callback’ => ‘my_create_item’,
‘permission_callback’ => fn() =>
current_user_can(‘edit_posts’),
],
]
);
schema
Registering a Schema
Adding a
schema key to your route gives clients machine-readable documentation and enables WordPress to validate args automatically.register_rest_route( ‘myplugin/v1’, ‘/items’,
[
‘methods’ => ‘GET’,
‘callback’ => ‘my_get_items’,
‘permission_callback’ => ‘__return_true’,
‘schema’ => function() {
return [
‘$schema’ => ‘http://json-schema.org/draft-04/schema#’,
‘title’ => ‘item’,
‘type’ => ‘object’,
‘properties’ => [
‘id’ => [ ‘type’ => ‘integer’ ],
‘title’ => [ ‘type’ => ‘string’ ],
],
];
},
]
);
Handling Requests & Responses
request
WP_REST_Request — Reading Data
The request object is injected into every callback. Use its methods to safely read query params, body data, and headers.
function my_callback( WP_REST_Request $request ) {
// Query string: ?status=publish
$status = $request->get_param( ‘status’ );
// JSON body
$body = $request->get_json_params();
// All params (query + body merged)
$all = $request->get_params();
// Header
$ct = $request->get_header( ‘content-type’ );
}
response
WP_REST_Response — Sending Data
Always wrap your return data in a proper response. Set status codes and headers for full HTTP compliance.
function my_callback( $request ) {
$data = [ ‘id’ => 1, ‘title’ => ‘Hello’ ];
$response = new WP_REST_Response( $data, 200 );
$response->header( ‘X-Total-Count’, 42 );
return $response;
}
// Shorthand for simple cases:
return rest_ensure_response( $data );
response
Returning Errors
Return a
WP_Error from any callback or permission check to send a structured JSON error response with the correct HTTP status code.function my_callback( $request ) {
$id = $request->get_param( ‘id’ );
$post = get_post( $id );
if ( ! $post ) {
return new WP_Error(
‘rest_post_invalid_id’,
‘Invalid post ID.’,
[ ‘status’ => 404 ]
);
}
if ( $post->post_status !== ‘publish’ ) {
return new WP_Error(
‘rest_forbidden’,
‘You cannot view this post.’,
[ ‘status’ => 403 ]
);
}
return rest_ensure_response( $post );
}
request
Argument Validation & Sanitization
Define
validate_callback and sanitize_callback per argument to keep your callback clean and secure without manual checks.‘args’ => [
’email’ => [
‘required’ => true,
‘type’ => ‘string’,
‘validate_callback’ => function( $val ) {
return is_email( $val );
},
‘sanitize_callback’ => ‘sanitize_email’,
],
‘count’ => [
‘type’ => ‘integer’,
‘default’ => 10,
‘minimum’ => 1,
‘maximum’ => 100,
],
],
Authentication Methods
auth
Cookie Authentication
Built into WordPress. Works for logged-in users making requests from the same domain (e.g. admin-ajax or front-end JS). Requires a nonce for write operations.
// In PHP — localize the nonce
wp_localize_script( ‘my-script’, ‘wpApiSettings’, [
‘root’ => esc_url_raw( rest_url() ),
‘nonce’ => wp_create_nonce( ‘wp_rest’ ),
]);
// In JS — send it with every request
fetch( wpApiSettings.root + ‘wp/v2/posts’, {
headers: {
‘X-WP-Nonce’: wpApiSettings.nonce,
},
});
auth
Application Passwords
Built in since WP 5.6. Generate per-app passwords from the user profile screen. Send via HTTP Basic Auth — ideal for external apps, CLI tools, and server-to-server calls.
// Generate in WP Admin → Users → Profile
// → Application Passwords → Add New
// Use in HTTP Basic Auth header:
const creds = btoa( ‘username:app-password’ );
fetch( ‘https://site.com/wp-json/wp/v2/posts’, {
method: ‘POST’,
headers: {
‘Authorization’: `Basic ${creds}`,
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify({
title: ‘New Post’,
status: ‘publish’,
}),
});
auth
JWT Authentication
Not built in — requires a plugin (e.g. JWT Auth by Useful Team). Client exchanges credentials for a token, then sends it as a Bearer header on each request.
// 1. Obtain token
const res = await fetch(‘/wp-json/jwt-auth/v1/token’, {
method: ‘POST’,
body: JSON.stringify({
username: ‘user’,
password: ‘pass’,
}),
});
const { token } = await res.json();
// 2. Use token in subsequent requests
fetch(‘/wp-json/wp/v2/posts’, {
headers: {
‘Authorization’: `Bearer ${token}`,
},
});
auth
Custom Permission Callbacks
The
permission_callback is your gatekeeper. Return true to allow, false for a 403, or a WP_Error for a custom message. Never use __return_true for sensitive data.‘permission_callback’ => function( $request ) {
// Public endpoint — anyone can read
if ( $request->get_method() === ‘GET’ ) {
return true;
}
// Write operations — must be logged in
if ( ! is_user_logged_in() ) {
return new WP_Error(
‘rest_not_logged_in’,
‘You must be logged in.’,
[ ‘status’ => 401 ]
);
}
// Must have capability
return current_user_can( ‘edit_posts’ );
},
Extending Existing Endpoints
extend
register_rest_field()
Add custom fields to existing REST responses — like posts, users, or terms — without replacing the whole endpoint. Define get, update, and schema callbacks.
register_rest_field( ‘post’, ‘featured_label’, [
‘get_callback’ => function( $post ) {
return get_post_meta(
$post[‘id’], ‘_featured_label’, true
);
},
‘update_callback’ => function( $val, $post ) {
update_post_meta(
$post->ID, ‘_featured_label’,
sanitize_text_field( $val )
);
},
‘schema’ => [
‘type’ => ‘string’,
‘description’ => ‘A featured label for the post.’,
‘context’ => [ ‘view’, ‘edit’ ],
],
]);
extend
show_in_rest for CPTs
Custom post types and taxonomies are hidden from the REST API by default. Set
show_in_rest to true (and optionally rest_base) when registering them.register_post_type( ‘project’, [
‘label’ => ‘Projects’,
‘public’ => true,
‘show_in_rest’ => true,
‘rest_base’ => ‘projects’,
// Optionally customize the controller:
// ‘rest_controller_class’ => ‘WP_REST_Posts_Controller’,
]);
register_taxonomy( ‘genre’, ‘project’, [
‘label’ => ‘Genres’,
‘show_in_rest’ => true,
‘rest_base’ => ‘genres’,
]);
extend
Modifying Responses — rest_prepare_*
Use the
rest_prepare_{post_type} filter to modify or strip data from existing endpoint responses before they’re sent to the client.add_filter(
‘rest_prepare_post’,
function( $response, $post, $request ) {
// Remove a field from the response
$data = $response->get_data();
unset( $data[‘guid’] );
unset( $data[‘link’] );
// Add computed data
$data[‘reading_time’] =
ceil( str_word_count(
strip_tags( $post->post_content )
) / 200 );
$response->set_data( $data );
return $response;
},
10, 3
);
extend
Filtering REST Queries
Use
rest_post_query (or the equivalent for other types) to modify the underlying WP_Query args before results are fetched.add_filter(
‘rest_post_query’,
function( $args, $request ) {
// Only return posts with a meta value set
if ( $request->get_param( ‘featured_only’ ) ) {
$args[‘meta_key’] = ‘_featured’;
$args[‘meta_value’] = ‘1’;
}
return $args;
},
10, 2
);
Consuming the API — JavaScript Patterns
JS
Fetch with Pagination
The REST API uses
X-WP-Total and X-WP-TotalPages response headers for pagination metadata. Read them to build a proper paginator.async function getPosts( page = 1 ) {
const res = await fetch(
`/wp-json/wp/v2/posts?per_page=10&page=${page}`
);
const total = res.headers.get(‘X-WP-Total’);
const totalPages = res.headers.get(‘X-WP-TotalPages’);
const posts = await res.json();
return { posts, total, totalPages };
}
JS
@wordpress/api-fetch
The official WP package for REST requests in block and Gutenberg contexts. Automatically handles nonces, root URL, and middleware — the right tool for block development.
import apiFetch from ‘@wordpress/api-fetch’;
// GET
const posts = await apiFetch({
path: ‘/wp/v2/posts?per_page=5’,
});
// POST
const newPost = await apiFetch({
path: ‘/wp/v2/posts’,
method: ‘POST’,
data: {
title: ‘Hello from blocks’,
status: ‘publish’,
},
});
JS
Embedding Related Data
Add
_embed to any request to inline related resources (author, featured media, terms) and avoid extra round-trips to the server.const res = await fetch(
‘/wp-json/wp/v2/posts?_embed’
);
const posts = await res.json();
posts.forEach( post => {
// Author object — no extra request needed
const author = post._embedded?.author?.[0];
// Featured image
const img =
post._embedded?.[‘wp:featuredmedia’]?.[0];
console.log( author?.name, img?.source_url );
});
JS
Sparse Fieldsets — _fields
Use
_fields to request only the properties you need. Reduces payload size significantly — essential for performance-critical front ends.// Only get id, title, slug, and date
const res = await fetch(
‘/wp-json/wp/v2/posts’ +
‘?_fields=id,title,slug,date’ +
‘&per_page=20’
);
const posts = await res.json();
// Each post is now a lightweight object:
// { id, title: { rendered }, slug, date }
⚠ Pro Tips
- Always set a meaningful namespace and version in your route:
myplugin/v1— this avoids conflicts and makes deprecation easier down the road. - Never use
__return_trueas apermission_callbackfor endpoints that write or expose private data. It’s fine for truly public read endpoints only. - Use
_fieldsin every front-end fetch to trim response payloads — the default WP post response can be 5–10x larger than what you actually need. - Validate and sanitize all args at the route definition level using
validate_callbackandsanitize_callback— keep your callback logic clean. - Use
rest_url()andwp_localize_script()to pass the API root and nonce to JS — never hardcode the URL. - For Gutenberg / block development, always use
@wordpress/api-fetchover rawfetch()— it handles middleware, auth, and root URL automatically.
Helpful Resources & Links
WP REST API Handbook
The authoritative official guide covering every aspect of the WP REST API from basics to advanced.
developer.wordpress.org/rest-api
Endpoint Reference
Full reference of every built-in endpoint, its args, responses, and schema — bookmarkable.
developer.wordpress.org/rest-api/reference
WP-API.org
Community hub and original project site for the WP REST API with guides and client libraries.
wp-api.org
WP Engine — REST API Guide
Practical deep-dive into REST API performance, caching strategies, and headless WP patterns.
wpengine.com/resources
@wordpress/api-fetch
Source and docs for the official WP JS package for REST API requests — the right tool for block development.
github.com/WordPress/gutenberg
Testing with RapidAPI
Use RapidAPI or Hoppscotch as a GUI client to test and explore your WP REST endpoints interactively.
rapidapi.com
ACF + REST API
How to expose Advanced Custom Fields data through the REST API for headless WordPress builds.
roots.io/guides
Auth Deep Dive
An excellent breakdown of every WP REST API authentication method and when to use each one.
jjj.blog