Rest API

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_true as a permission_callback for endpoints that write or expose private data. It’s fine for truly public read endpoints only.
  • Use _fields in 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_callback and sanitize_callback — keep your callback logic clean.
  • Use rest_url() and wp_localize_script() to pass the API root and nonce to JS — never hardcode the URL.
  • For Gutenberg / block development, always use @wordpress/api-fetch over raw fetch() — it handles middleware, auth, and root URL automatically.
Helpful Resources & Links