REST API
The WordPress REST API exposes posts, pages, media, users, and any CPT/taxonomy with show_in_rest over JSON. Authenticate with cookies, Application Passwords, or JWT plugins.
Endpoints, custom routes, auth, react
EXAMPLE
<?php
// 1) Out of the box — discover the API
// GET /wp-json/ ← index
// GET /wp-json/wp/v2/posts ← published posts
// GET /wp-json/wp/v2/posts/123
// GET /wp-json/wp/v2/posts?per_page=10&search=docker&_embed
// GET /wp-json/wp/v2/categories
// GET /wp-json/wp/v2/users
// 2) Register a CUSTOM endpoint
add_action('rest_api_init', function () {
register_rest_route('myapp/v1', '/featured-posts', [
'methods' => 'GET',
'callback' => 'myapp_featured_posts',
'permission_callback' => '__return_true', // public
'args' => [
'count' => [
'default' => 5,
'sanitize_callback' => 'absint',
'validate_callback' => fn($v) => $v >= 1 && $v <= 20,
],
],
]);
register_rest_route('myapp/v1', '/subscribe', [
'methods' => 'POST',
'callback' => 'myapp_subscribe',
'permission_callback' => fn() => is_user_logged_in(),
'args' => [
'email' => [
'required' => true,
'sanitize_callback' => 'sanitize_email',
'validate_callback' => 'is_email',
],
],
]);
});
function myapp_featured_posts(WP_REST_Request $req) {
$count = $req->get_param('count');
$query = new WP_Query([
'posts_per_page' => $count,
'meta_query' => [['key' => 'featured', 'value' => '1']],
]);
$out = [];
while ($query->have_posts()) {
$query->the_post();
$out[] = [
'id' => get_the_ID(),
'title' => get_the_title(),
'excerpt' => get_the_excerpt(),
'url' => get_permalink(),
'thumb' => get_the_post_thumbnail_url(null, 'medium'),
'author' => get_the_author(),
'posted_at'=> get_the_date('c'),
];
}
wp_reset_postdata();
return rest_ensure_response($out);
}
function myapp_subscribe(WP_REST_Request $req) {
$email = $req->get_param('email');
// …add to mailing list…
return new WP_REST_Response(['ok' => true], 201);
}
// 3) Expose a custom field on a CPT — register_meta + register_rest_field
register_post_meta('product', 'price', [
'type' => 'number',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'floatval',
]);
// Computed field (read-only)
add_action('rest_api_init', function () {
register_rest_field('product', 'price_with_tax', [
'get_callback' => function ($obj) {
$price = (float) get_post_meta($obj['id'], 'price', true);
return round($price * 1.1, 2);
},
]);
});
// 4) Authentication
// a) Cookies — same-origin only (built in)
// b) Application Passwords — Settings → Users → Application Passwords
// curl -u 'username:xxxx xxxx xxxx xxxx xxxx xxxx' https://site/wp-json/wp/v2/posts
// c) JWT — install a JWT auth plugin; useful for cross-origin SPAs
// d) OAuth 2.0 — for third-party apps
// 5) CORS — for cross-origin SPAs
add_action('rest_api_init', function () {
remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
add_filter('rest_pre_serve_request', function ($value) {
header('Access-Control-Allow-Origin: https://app.example.com');
header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce');
header('Access-Control-Allow-Credentials: true');
return $value;
});
}, 15);
// 6) Front-end (React) — fetch + nonce for cookie auth
function Posts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/wp-json/wp/v2/posts?per_page=10&_embed', {
credentials: 'include',
headers: { 'X-WP-Nonce': window.myappNonce },
})
.then(r => r.json())
.then(setPosts);
}, []);
return posts.map(p => (
<article key={p.id}>
<h2 dangerouslySetInnerHTML={{ __html: p.title.rendered }} />
<div dangerouslySetInnerHTML={{ __html: p.excerpt.rendered }} />
</article>
));
}
// Provide the nonce from PHP
wp_localize_script('myapp-app', 'myappNonce', wp_create_nonce('wp_rest'));
// 7) Pagination — headers + body
// X-WP-Total — total items
// X-WP-TotalPages — total pages
// Use ?page=2 to fetch next page
// 8) Best practices
// • Always sanitize + validate args via the route definition
// • Set permission_callback EXPLICITLY (never just `true` unless truly public)
// • Add caching headers (Cache-Control, ETag) for high-traffic endpoints
// • Don't expose internal IDs; use slugs / UUIDs where they make URLs cleaner
// • Read-only by default; require POST/PUT/DELETE for state changes
// 9) Headless WordPress patterns
// • WP backend → REST API → Next.js / Nuxt / SvelteKit front-end
// • WPGraphQL plugin for GraphQL endpoint instead of REST
// • Cache the REST responses at CDN edge (long max-age + revalidate via webhooks)
Why it matters
For headless WordPress, the REST API + WPGraphQL + a CDN at the edge is the modern stack. The trick is in permission_callback — always set it explicitly, never default to public on writes.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// /wp-json/wp/v2/posts
register_rest_route('myplugin/v1', '/hello', [
'methods' => 'GET',
'callback' => fn() => ['msg' => 'hi'],
]);
Try it Yourself »
Discussion
Loading…