WordPress REST API: Complete Developer Guide 2026
The WordPress REST API is the JSON interface that turns WordPress into a data backend. It exposes posts, pages, users, taxonomies, media, custom post types, and custom fields as URL-addressable resources you can read and write from any HTTP client. Every modern WordPress integration uses it: the Block Editor talks to wp-admin through it, the mobile app reads through it, headless frontends fetch through it, and external tools like Zapier or Make push content through it. Understanding the WordPress REST API is no longer optional for serious WordPress development.
This guide covers what the WordPress REST API actually is, the default endpoint structure, the three authentication methods that work for 2026 (Application Passwords, JWT, OAuth 1.0a), how to register custom endpoints with register_rest_route, exposing custom post types and fields, pagination and filtering on collections, performance considerations on large queries, and the headless WordPress patterns that depend on the API. By the end you’ll know how to consume and extend the WordPress REST API with confidence.
What the WordPress REST API is
The WordPress REST API is a built-in HTTP/JSON interface that ships with WordPress core since version 4.7 (December 2016). It exposes WordPress content as URL-addressable resources following REST conventions: GET to read, POST to create, PUT/PATCH to update, DELETE to remove. The base URL is always https://yoursite.com/wp-json/, and the namespace for core endpoints is wp/v2. So https://yoursite.com/wp-json/wp/v2/posts returns the latest 10 posts as JSON.
The WordPress REST API replaces XML-RPC for most use cases. It’s faster (JSON parses faster than XML), simpler (no SOAP envelopes), discoverable (every endpoint self-documents through OPTIONS requests), and extensible (every plugin can register its own routes). XML-RPC still ships with WordPress for backward compatibility but the REST API is the modern path forward.

Default WordPress REST API endpoints
WordPress core ships with controllers for every built-in content type. Hit any of these from a browser to see the JSON output (replace yoursite.com).
| Resource | Endpoint | Methods |
|---|---|---|
| Posts | /wp/v2/posts | GET, POST, PUT, DELETE |
| Single post | /wp/v2/posts/{id} | GET, POST, DELETE |
| Pages | /wp/v2/pages | GET, POST, PUT, DELETE |
| Media | /wp/v2/media | GET, POST, DELETE |
| Categories | /wp/v2/categories | GET, POST, PUT, DELETE |
| Tags | /wp/v2/tags | GET, POST, PUT, DELETE |
| Users | /wp/v2/users | GET, POST, PUT, DELETE |
| Comments | /wp/v2/comments | GET, POST, PUT, DELETE |
| Settings | /wp/v2/settings | GET, POST |
| Block types | /wp/v2/block-types | GET |
| Search | /wp/v2/search | GET |
| Site index | / (root) | GET (returns route discovery) |
# Read the latest 10 posts (no auth needed, public endpoint)
curl https://gatilab.com/wp-json/wp/v2/posts
# Read a specific post
curl https://gatilab.com/wp-json/wp/v2/posts/1055035
# Read with embedded author and featured media (faster than 3 round-trips)
curl 'https://gatilab.com/wp-json/wp/v2/posts/1055035?_embed'
# Filter posts by category, tag, search, date
curl 'https://gatilab.com/wp-json/wp/v2/posts?categories=147&per_page=20&orderby=date&order=desc'
# Discover every endpoint your install exposes
curl https://gatilab.com/wp-json/ | jq '.routes | keys'
The ?_embed query parameter is underrated. Without it, fetching a post returns numeric IDs for author, featured_media, and categories. With it, the response includes the full author object, the full media object with all sizes, and the category objects. One request replaces four. Use _embed in headless frontends to cut your API call count by 70%.
WordPress REST API authentication methods
Public read endpoints work without authentication. Anything that writes (POST, PUT, DELETE) or reads private data (drafts, user details, settings) requires auth. Three methods work in 2026: Application Passwords (built-in since WP 5.6), JWT (via plugin), and OAuth 1.0a (via plugin).
Application Passwords (recommended)
Built into WordPress core. Each user can generate any number of application-specific passwords from their profile screen. Each password is a long random string that never expires until revoked. Authenticate with HTTP Basic Auth: send the username and the application password in the Authorization: Basic header. This is the auth method I use for every server-to-server WordPress integration.
# Create a new draft post via Application Password auth
curl -X POST https://gatilab.com/wp-json/wp/v2/posts \
-u "wpgaurav:abcd 1234 efgh 5678 ijkl 9012" \
-H "Content-Type: application/json" \
-d '{
"title": "Hello from cURL",
"content": "<p>Posted via the WordPress REST API.</p>",
"status": "draft"
}'
Spaces in the application password are intentional and preserved in the curl command. WordPress strips the spaces server-side before comparing. Application passwords are revocable per-application, so if a key leaks you only revoke that one without changing the user’s main login password.
JWT (JSON Web Tokens)
JWT is the right choice when your client is a browser-based JavaScript app and you can’t send Basic Auth headers from a public origin. Install the JWT Authentication for WP REST API plugin or the JWT Auth plugin. Both expose /wp-json/jwt-auth/v1/token which exchanges username + password for a signed token. Subsequent requests send Authorization: Bearer {token} instead of Basic.
# Get a token
curl -X POST https://gatilab.com/wp-json/jwt-auth/v1/token \
-H "Content-Type: application/json" \
-d '{"username":"wpgaurav","password":"my_login_password"}'
# Response:
# {
# "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
# "user_email": "[email protected]",
# "user_nicename": "wpgaurav",
# "user_display_name": "Gaurav"
# }
# Use the token on subsequent requests
curl https://gatilab.com/wp-json/wp/v2/users/me \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
JWT tokens expire (default 7 days). Your client refreshes them by re-authenticating. Tokens can be blacklisted server-side by setting up a token revocation list, but the simpler approach is just letting them expire and forcing re-auth.
OAuth 1.0a
OAuth 1.0a is the right choice when third-party apps need to act on behalf of WordPress users without ever seeing their passwords. The WP REST API OAuth1 plugin from the WordPress core team handles the dance. Most modern integrations use Application Passwords or JWT instead because OAuth 1.0a’s HMAC-SHA1 signing is fiddly. Use OAuth 1.0a if your platform requires it (legacy enterprise integrations) or if you’re publishing a marketplace app where end users grant access to their own WordPress sites.
Always use HTTPS when calling the WordPress REST API with credentials. Application Passwords sent over plain HTTP can be read on the wire. Every modern host (the ones in our best managed WordPress hosting roundup) ships free Let’s Encrypt SSL by default; there’s no reason to run plain HTTP in 2026.
Custom WordPress REST API endpoints with register_rest_route
The real power of the WordPress REST API is how easy it is to add your own endpoints. The pattern is one function call: register_rest_route() on the rest_api_init action.
add_action('rest_api_init', function () {
register_rest_route('gatilab/v1', '/stats/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => 'gl_get_post_stats',
'permission_callback' => '__return_true', // public, no auth
'args' => [
'id' => [
'validate_callback' => fn($p) => is_numeric($p),
'sanitize_callback' => 'absint',
],
],
]);
});
function gl_get_post_stats(WP_REST_Request $request) {
$id = $request['id'];
if (!get_post($id)) {
return new WP_Error('not_found', 'Post not found', ['status' => 404]);
}
return rest_ensure_response([
'id' => $id,
'views' => (int) get_post_meta($id, 'view_count', true),
'shares' => (int) get_post_meta($id, 'share_count', true),
'comments' => get_comments_number($id),
]);
}
That endpoint is now live at /wp-json/gatilab/v1/stats/123. It validates that the ID is numeric, sanitizes it with absint, looks up the post, and returns view/share/comment counts as JSON. Three things make register_rest_route powerful: regex-based URL parameters (the (?P<id>\d+) capture group), per-arg validation and sanitization, and the WP_REST_Request object that gives you a clean API to read query params, body, and headers.
Always set permission_callback. WordPress 5.5+ throws a warning in the error log if you don’t, and 6.0+ may block requests on stricter configurations. Use __return_true for public endpoints, a custom function for capability checks, or 'is_user_logged_in' for any-logged-in-user access.
Exposing custom post types and fields via the WordPress REST API
// Custom post type with REST support
register_post_type('book', [
'public' => true,
'has_archive' => true,
'supports' => ['title', 'editor', 'thumbnail'],
'show_in_rest' => true, // exposes /wp/v2/books
'rest_base' => 'books', // override the URL slug
'rest_controller_class' => 'WP_REST_Posts_Controller',
]);
// Expose a custom field on a post (or any object)
add_action('rest_api_init', function () {
register_rest_field('post', 'reading_time', [
'get_callback' => function ($post_arr) {
return (int) get_post_meta($post_arr['id'], 'reading_time', true);
},
'update_callback' => function ($value, $post) {
return update_post_meta($post->ID, 'reading_time', absint($value));
},
'schema' => [
'description' => 'Estimated reading time in minutes',
'type' => 'integer',
'context' => ['view', 'edit'],
],
]);
});
The show_in_rest argument on register_post_type is the single line that turns a custom post type into a REST resource. Without it, your CPT exists in wp-admin but is invisible to the API. The full custom post type API is covered in our WordPress custom post types guide.

Pagination and filtering on REST collections
Every collection endpoint supports the same query parameters for pagination and filtering. Memorize these and you’ll rarely need to extend a core endpoint.
| Parameter | What it does | Example |
|---|---|---|
per_page | Items per page (1-100, default 10) | ?per_page=50 |
page | Page number (1-based) | ?page=3 |
offset | Skip N items | ?offset=20 |
orderby | Sort field | ?orderby=date|title|menu_order |
order | Sort direction | ?order=asc|desc |
search | Free-text search | ?search=wordpress |
after / before | Date range | ?after=2026-01-01T00:00:00 |
categories | Filter by category IDs | ?categories=5,12 |
tags | Filter by tag IDs | ?tags=42 |
_fields | Sparse fieldsets (cuts response size) | ?_fields=id,title,link |
_fields is the underused performance trick. WordPress’s default response includes 30+ fields per post including the full rendered content. If your headless frontend only needs id, title, and slug for a sitemap, requesting ?_fields=id,title,slug,link drops the response from 50 KB to under 2 KB per page. On a 5,000-post sitemap build, that’s the difference between a 4-minute build and a 30-second build.
Pagination headers (X-WP-Total and X-WP-TotalPages) give you the full collection size and page count without parsing the response body. Read them with response.headers.get('X-WP-Total') on the client side and use them to drive pagination UI.
Performance: caching the WordPress REST API
The default WordPress REST API has no built-in cache. Every request rebuilds the response by hitting the database, running the_content filters, and re-serializing. On a busy headless site, this becomes the bottleneck before WordPress’s own template rendering does.
- Page cache at the edge. Cloudflare, Vercel, or Cloudflare Workers cache GET responses by URL. Set
Cache-Control: public, max-age=300headers on read endpoints. This single change handles 95% of REST traffic without touching WordPress. - Object cache for repeated queries. Redis or Memcached object cache (via the WP Redis or W3 Total Cache plugins) caches the raw query results, cutting database round-trips on miss-cache requests.
- Sparse fieldsets via
_fields. Already covered. Cuts response size and serialization time. - Disable embedding when not needed. The
_embedparameter is convenient but adds 3-5x serialization cost. Use it sparingly. - Custom endpoint with raw SQL. When even a tuned default endpoint is too slow, register a custom route that queries the database directly with $wpdb->prepare() and returns only the fields you need. I’ve cut category-archive load times from 800ms to 40ms with this technique.
For full-page caching at the headless layer, see our headless WordPress guide, which covers Vercel/Cloudflare edge caching patterns, ISR (incremental static regeneration), and on-demand rebuilds tied to WordPress webhook events.
Headless WordPress patterns using the REST API
Headless WordPress uses the WordPress REST API as the data source for a separate frontend. Three patterns dominate the 2026 landscape.
- Static generation at build time. Next.js getStaticProps, Astro’s getStaticPaths, or Eleventy’s data files fetch from the WordPress REST API during the build. Output is static HTML deployed to a CDN. Best for content-heavy sites where rebuild lag is acceptable.
- Incremental Static Regeneration (ISR). Next.js or Vercel revalidates pages on demand or on a timer. WordPress fires a webhook on save_post that triggers a single-page rebuild. Best for blogs and marketing sites where editors expect changes to go live in seconds.
- Edge-rendered or server-rendered. The frontend fetches from WordPress on every request, with edge caching in front. Best for personalized content (logged-in users, A/B variants, dashboards).
For all three, the WordPress REST API is the contract between WordPress and the frontend. Stable URL structure, versioned namespaces (wp/v2 guarantees backward compat within v2), and JSON Schema descriptions of every response make it dependable for production frontends.
For development workflows that exercise the API heavily, the WP-CLI commands cheatsheet includes commands for testing routes, and the WordPress hooks tutorial covers the rest_api_init action in depth. If you’re choosing where to host a WordPress install that exposes the REST API at scale, see the best managed WordPress hosting roundup.
FAQs about the WordPress REST API
Is the WordPress REST API enabled by default?
Yes. Since WordPress 4.7, the REST API is part of core and enabled on every install. Public read endpoints (posts, pages, media) work without any setup. Some security plugins (Wordfence, iThemes Security) can disable specific routes; check your plugin settings if /wp-json returns 401 or 404 unexpectedly.
How do I disable the WordPress REST API for non-logged-in users?
Don’t, unless you have a specific reason. Block Editor, Site Health, and several core features rely on REST API access. If you must restrict, hook the rest_authentication_errors filter and return a WP_Error for non-authenticated requests. Most security plugins offer this as a toggle, but it breaks the Block Editor for logged-out preview workflows.
What’s the difference between WordPress REST API and WPGraphQL?
REST API is built-in, returns JSON over predefined endpoints, and tends to over-fetch (full payloads). WPGraphQL is a plugin that adds a GraphQL endpoint where you specify exactly which fields you want. WPGraphQL is faster for nested queries (one request for post + author + categories vs three) and pairs well with TypeScript codegen. REST is simpler and has zero dependencies. Both work for headless. I default to WPGraphQL for new builds.
How do I authenticate the WordPress REST API from JavaScript?
From the WordPress admin (Block Editor or admin-side plugin), use the wpApiSettings.nonce that WordPress automatically exposes. Set the X-WP-Nonce header on every request. From a separate JS frontend (headless), use JWT or Application Passwords. Browser-based clients should never embed Application Passwords in client-side code; use a server proxy or JWT instead.
What’s the per_page limit on WordPress REST API collections?
The default cap is 100 items per request. Higher values are rejected to prevent server overload. To fetch all posts, paginate by passing page=1, page=2, etc. and respect the X-WP-TotalPages header. Some plugins raise the cap via the rest_post_collection_params filter, but going above 100 usually means you should be using a custom endpoint with sparse fields instead.
How do I expose ACF fields through the WordPress REST API?
Two options. ACF Pro 5.11+ has built-in REST support: enable show_in_rest on the field group and ACF fields appear under the acf key on every post response. For older ACF versions, install the ACF to REST API plugin (free) which adds the same support. For complex setups, register_rest_field gives you full control over how fields appear in the response.
Can I write to the WordPress REST API from another WordPress site?
Yes. Use wp_remote_post() with HTTP Basic Auth headers. Generate an Application Password on the destination site for a user with edit_posts or higher capability. The classic use case is cross-site content syndication: a hub WordPress publishes once and POSTs to spoke sites’ REST API. WP-CLI is a useful diagnostic tool: try the same operation with curl first, then port to wp_remote_post.
What happens if the WordPress REST API returns 401 Unauthorized?
The Authorization header is missing, malformed, or your credentials are wrong. Check three things: the username (use wp_username, not the display name), the application password format (spaces are kept, no other punctuation), and that your host isn’t stripping Authorization headers (some shared hosts do this; add SetEnvIf Authorization “(.+)” HTTP_AUTHORIZATION=$1 to .htaccess). For JWT, confirm the token hasn’t expired.
Common WordPress REST API errors and fixes
Five errors account for most “the WordPress REST API isn’t working” support tickets.
- 401 Unauthorized. Your auth header is missing or wrong. On Apache, some shared hosts strip the Authorization header. Add
SetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1to .htaccess. Verify the header survived withprint_r(getallheaders())in a test endpoint. - 403 rest_forbidden. Your user lacks the capability for the action. Posting to /wp/v2/posts requires edit_posts. Check
wp user get {id} --field=rolesover CLI to confirm. - 404 No route was found. The route isn’t registered. Either you didn’t run rest_api_init, the namespace is wrong, or a security plugin is blocking it. Hit
/wp-json/and check the routes object for your endpoint. - 500 internal error. Your callback threw an exception or returned malformed data. Tail wp-content/debug.log after enabling WP_DEBUG_LOG. The stack trace points at the line.
- CORS errors in the browser. Cross-origin JS clients hit the WordPress REST API and get blocked. Add an Access-Control-Allow-Origin header via the rest_pre_serve_request filter, or proxy through your headless frontend’s API routes.
For systematic API debugging, install the WP REST API tester from the WordPress.org repo (or use Postman with a saved collection). Both let you replay requests, inspect response headers, and confirm auth without retyping curl commands.
Schema and self-documentation
Every endpoint registered through register_rest_route can declare a JSON Schema describing its arguments and response shape. WordPress core does this for every built-in route. The schema powers two things: automatic argument validation (via the schema’s type/required/enum keys), and self-documentation through OPTIONS requests.
# OPTIONS request returns the full schema for an endpoint
curl -X OPTIONS https://gatilab.com/wp-json/wp/v2/posts | jq
# Response includes:
# - Available methods (GET, POST, etc.)
# - Required and optional args with types
# - Response schema with every field documented
# - Links to related routes
Tools like the @wordpress/api-fetch package use schema discovery to generate TypeScript types for the WordPress REST API responses. WPGraphQL bypasses this by exposing a full GraphQL schema instead, but for REST-only setups, OPTIONS-based discovery is the right pattern.
Bottom line on the WordPress REST API
The WordPress REST API is the modern interface to every WordPress install. Read public endpoints freely, authenticate with Application Passwords for server-to-server work, register custom routes with register_rest_route, expose CPTs via show_in_rest, paginate with per_page and _fields, cache aggressively at the edge. Once these patterns are wired into muscle memory, integrating WordPress with any external system becomes a 30-minute job rather than a 30-day one.