Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 017 / 100

WordPress REST API tutorial: what developers should know

Read and write content over HTTP.

Expona Admin7 min read

WordPress REST API tutorial: what developers should know

WordPress powers over 43% of all websites, but many teams still treat it as a monolithic CMS instead of a headless backend. The WordPress REST API transforms that dynamic by exposing your entire content model over HTTP, enabling you to build decoupled frontends, mobile apps, and third-party integrations. Whether you're building a React storefront, a Next.js blog, or integrating WordPress data into an external dashboard, understanding the REST API is essential.

In this WordPress REST API tutorial, we'll explore what developers should know to read and write content over HTTP effectively. You'll learn how to leverage built-in endpoints, authenticate external clients, surface custom fields, and handle pagination correctly. These fundamentals will unlock faster development cycles and more flexible architectures.

Built-in endpoints — /wp-json/wp/v2/posts and more

The WordPress REST API automatically exposes standard resources at predictable endpoints. When you install WordPress, the API is immediately available at /wp-json, and resources live under versioned paths like /wp/v2.

The most common endpoint you'll interact with is posts. A simple GET request retrieves published content:

php
GET /wp-json/wp/v2/posts

This returns an array of post objects with titles, content, metadata, and media links. By default, only published posts are returned to unauthenticated requests. Pages, custom post types, custom taxonomies, users, comments, and media all have similar endpoints.

Let's say you're building a decoupled Next.js blog that pulls articles from WordPress. Your frontend makes authenticated requests to retrieve draft posts for preview:

php
GET /wp-json/wp/v2/posts?status=draft&per_page=20

You can filter, search, and sort using query parameters. To get posts from a specific category or order by date:

php
GET /wp-json/wp/v2/posts?categories=5&orderby=date&order=desc&per_page=10

The response includes pagination headers (more on those later) and link headers pointing to first, last, next, and previous pages. This structure lets your frontend navigate large datasets efficiently without loading everything at once.

Custom post types registered with show_in_rest => true automatically get endpoints too. If you have a "products" custom post type, it appears at /wp-json/wp/v2/products with full CRUD operations if your authentication allows it.

Authentication — Application passwords for external clients

Not all API requests should be public. Editing posts, deleting comments, or accessing draft content requires authentication. WordPress offers several authentication methods, but application passwords are the modern standard for integrating external applications.

Application passwords are token-based credentials that are scoped, revocable, and separate from user passwords. They're the right choice when you need to authenticate headless frontends, mobile apps, or third-party services.

Generate an application password in WordPress Settings (Users → Your Profile → Application Passwords). You'll get a username and password pair that looks like: user:abcd efgh ijkl mnop (spaces included for readability).

From your external client, send a Basic Authentication header with every request:

php
Authorization: Basic base64_encode('user:abcd efgh ijkl mnop')

If you're using JavaScript on a Next.js frontend:

php
const credentials = btoa('user:abcd efgh ijkl mnop');
const response = await fetch('https://yoursite.com/wp-json/wp/v2/posts', {
  method: 'POST',
  headers: {
    'Authorization': `Basic ${credentials}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'New Post',
    content: 'Post content here',
    status: 'publish'
  })
});

Application passwords inherit the capabilities of the user who created them. If you generate an app password for an "Editor" user, external requests authenticated with that password can only perform actions editors can perform. This principle of least privilege keeps your WordPress installation secure.

For public-facing APIs where you don't want to expose user credentials, consider OAuth2 plugins or JWT (JSON Web Token) packages. But for internal integrations and decoupled frontends you control, application passwords are straightforward and sufficient.

Custom fields — register_rest_field or show_in_rest for meta

Standard post fields like title and content are great, but most real projects need custom data. You might store product SKUs, event dates, author bios, or feature flags as post meta. By default, meta fields don't appear in REST API responses.

There are two ways to expose custom fields: using show_in_rest when you register meta, or building a custom REST field.

The simplest approach is adding show_in_rest => true when you register meta with register_meta():

php
register_meta( 'post', 'product_sku', array(
  'type' => 'string',
  'description' => 'Product SKU',
  'single' => true,
  'show_in_rest' => true
) );

Now when you fetch a post, the response includes a meta object with your custom fields. You can read and write them like any other field:

php
POST /wp-json/wp/v2/posts/42
{
  "meta": {
    "product_sku": "SKU-12345"
  }
}

For more control—like transforming data, validating specific values, or exposing fields with different names—use register_rest_field():

php
register_rest_field( 'post', 'product_price', array(
  'get_callback' => function( $post ) {
    $price = get_post_meta( $post['id'], '_price', true );
    return (float) $price;
  },
  'update_callback' => function( $value, $post ) {
    if ( ! is_numeric( $value ) || $value < 0 ) {
      return new WP_Error( 'invalid_price', 'Price must be a positive number' );
    }
    update_post_meta( $post->ID, '_price', $value );
  },
  'schema' => array(
    'description' => 'Product price',
    'type' => 'number'
  )
) );

This approach lets you validate input, transform output, and document your schema. When a client sends a POST or PUT request with a price field, your callback validates it and stores it safely. When they fetch the post, your get_callback formats it for the response.

For e-commerce projects pulling product data into a Next.js storefront, this is critical. You control exactly what fields are exposed, how they're validated, and how they're transformed, ensuring your decoupled frontend gets clean, reliable data.

Pagination headers — X-WP-Total and X-WP-TotalPages

The REST API returns paginated results by default. A typical response fetches 10 items per page, but WordPress communicates the full dataset size through HTTP headers, not in the JSON body.

Every response includes headers like:

php
X-WP-Total: 247
X-WP-TotalPages: 25
Link: <https://yoursite.com/wp-json/wp/v2/posts?page=2>; rel="next"

The X-WP-Total header tells you how many items exist in total. X-WP-TotalPages tells you how many pages that represents at your current per_page setting. The Link header provides pre-built URLs for navigation.

When building a paginated list on your frontend, extract these headers to render pagination controls:

php
const fetchPosts = async (page = 1) => {
  const response = await fetch(
    `https://yoursite.com/wp-json/wp/v2/posts?page=${page}&per_page=20`
  );
  
  const posts = await response.json();
  const totalPages = parseInt(response.headers.get('X-WP-TotalPages'), 10);
  const total = parseInt(response.headers.get('X-WP-Total'), 10);
  
  console.log(`Showing page ${page} of ${totalPages} (${total} total posts)`);
  return { posts, totalPages, total };
};

This is essential for large datasets. If you have thousands of blog posts or comments, you never load them all at once. Instead, you paginate efficiently, respecting server and network resources. For a React blog pulling posts into a paginated archive, this pattern keeps your application responsive and your WordPress server healthy.

Common mistakes

We've seen teams make predictable errors when working with the REST API. Watch out for these pitfalls:

  • Ignoring authentication on write operations. Many developers test API writes locally without authentication, then push to production and wonder why external requests fail. Always test with real application passwords in a staging environment.
  • Assuming all meta is exposed. Custom post meta doesn't appear in REST responses unless you explicitly register it with show_in_rest or register_rest_field. This catches teams expecting data that never arrives.
  • Loading all results at once. Fetching /wp-json/wp/v2/posts without a per_page limit returns only 10 results. Beginners sometimes think they have all their data when they actually have 1 of 100 pages. Always check pagination headers and respect per_page limits.
  • Forgetting to enable REST for custom post types. If you register a custom post type without show_in_rest => true, it won't have an API endpoint. This is a common surprise when onboarding a new data model.
  • Storing sensitive data as meta. The REST API respects capabilities, but custom fields sometimes leak private information. Validate carefully what data should be exposed and to whom.

Key takeaways

  • The REST API lives at /wp-json and exposes posts, pages, custom post types, taxonomies, users, and comments through predictable, versioned endpoints like /wp/v2/posts.
  • Use application passwords for external client authentication—they're token-based, scoped, revocable, and follow WordPress capability rules.
  • Surface custom fields by adding show_in_rest => true to meta registration, or build custom REST fields with register_rest_field() for validation and transformation.
  • Pagination data lives in response headers: X-WP-Total, X-WP-TotalPages, and Link headers guide your frontend through large datasets efficiently.
  • The REST API is production-ready and widely used for headless WordPress, mobile apps, and third-party integrations—master it and you unlock architectural flexibility.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles