The WordPress REST API is a set of HTTP interfaces that allow external applications — whether JavaScript frontends, mobile apps, or third-party services — to read, write, and manage WordPress data directly over HTTP, exchanging information in JSON format. Introduced officially in WordPress 4.7, it has become the backbone of Headless WordPress architectures and Single-Page Applications that use WordPress purely as a content backend.

This guide walks you through the WordPress REST API from first principles: understanding the concept, exploring core endpoints, calling the API from JavaScript and PHP, creating custom routes, and applying security best practices before going live.

What Is the WordPress REST API and How Does It Work?

REST (Representational State Transfer) is an architectural style for web APIs that relies on standard HTTP verbs — GET, POST, PUT, PATCH, and DELETE — to communicate. WordPress implements this style through an interface that exposes every type of content on your site: Posts, Pages, Users, Media, Categories, Tags, Custom Post Types, and more.

When a client sends an HTTP request to a WordPress API endpoint, the server validates authentication (if required), processes the request, and returns data as JSON. Every endpoint lives under the /wp-json/ base path. For example:

The key benefit is a complete decoupling of frontend from backend. You can build a React, Vue, Next.js, or Flutter frontend and use WordPress solely for content storage and management accessed through the API — a pattern known as Headless WordPress.

Core Endpoints You Should Know

The primary namespace is wp/v2, which covers all built-in content types. The table below summarises the most commonly used endpoints:

Endpoint Method Description Auth Required
/wp/v2/posts GET List published posts No
/wp/v2/posts POST Create a new post Yes
/wp/v2/pages GET List published pages No
/wp/v2/users GET List users (public authors) Partial
/wp/v2/media GET / POST List or upload media files POST: Yes
/wp/v2/categories GET List categories No
/wp/v2/tags GET List tags No
/wp/v2/comments GET / POST List or create comments POST: Sometimes

You can discover every endpoint available on your specific installation by visiting https://yoursite.com/wp-json/, which returns a JSON discovery document listing all registered routes and namespaces — including those added by plugins.

Calling the API with JavaScript (Fetch API)

Fetching data from the WordPress REST API with JavaScript is straightforward using the native Fetch API. The example below retrieves the five most recent posts and logs their titles and URLs:

// Fetch the 5 most recent posts
async function getLatestPosts() {
  const response = await fetch(
    'https://yoursite.com/wp-json/wp/v2/posts?per_page=5&_fields=id,title,link,excerpt'
  );

  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }

  const posts = await response.json();

  posts.forEach(post => {
    console.log(post.id, post.title.rendered, post.link);
  });
}

getLatestPosts();

Notice the _fields query parameter — it instructs WordPress to return only the specified fields rather than the full 30+ field object, significantly reducing payload size and improving performance.

Useful Query Parameters

The REST API supports a rich set of standard query parameters for filtering, sorting, and paginating results:

// Filter posts by category, sorted alphabetically
const url = new URL('https://yoursite.com/wp-json/wp/v2/posts');
url.searchParams.set('categories', '5,8');
url.searchParams.set('per_page', '10');
url.searchParams.set('orderby', 'title');
url.searchParams.set('order', 'asc');
url.searchParams.set('_fields', 'id,title,link,date');

const res = await fetch(url.toString());
const data = await res.json();

Authentication with Application Passwords

Endpoints that modify data — creating, editing, or deleting content — require authentication. WordPress supports several methods, but the recommended approach for most integrations is Application Passwords, introduced in WordPress 5.6.

Generating an Application Password

  1. Go to WordPress Admin > Users > Your Profile.
  2. Scroll down to the "Application Passwords" section.
  3. Enter a name for your application (e.g. "My Headless App") and click "Add New Application Password".
  4. WordPress generates a password shown only once — copy and store it securely immediately.
// Create a post using Basic Auth with Application Password
const username = 'your_username';
const appPassword = 'xxxx xxxx xxxx xxxx xxxx xxxx';

const credentials = btoa(`${username}:${appPassword.replace(/\s/g, '')}`);

const response = await fetch('https://yoursite.com/wp-json/wp/v2/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Basic ${credentials}`
  },
  body: JSON.stringify({
    title: 'New Post via API',
    content: 'Post content goes here...',
    status: 'draft'
  })
});

const newPost = await response.json();
console.log('Created post ID:', newPost.id);

Security Tip: Never embed Application Passwords directly in frontend JavaScript — anyone can inspect the source or network requests and extract them. Store credentials on the server side and have your frontend communicate through a backend proxy endpoint. Also generate a separate Application Password per integration and revoke it immediately when no longer needed.

Calling the API from PHP

When consuming the WordPress REST API from PHP — whether in a plugin, a theme, or an external application — you have two good options: WordPress's own HTTP API functions (when inside a WP context) or plain cURL (for any PHP environment):

<?php
// Option 1: WordPress HTTP API (recommended inside WP)
$response = wp_remote_get(
    'https://yoursite.com/wp-json/wp/v2/posts?per_page=5',
    array(
        'timeout' => 15,
        'headers' => array( 'Accept' => 'application/json' )
    )
);

if ( is_wp_error( $response ) ) {
    error_log( 'API Error: ' . $response->get_error_message() );
    return false;
}

$posts = json_decode( wp_remote_retrieve_body( $response ), true );
foreach ( $posts as $post ) {
    echo $post['id'] . ': ' . $post['title']['rendered'] . "\n";
}

// Option 2: cURL (any PHP environment)
$ch = curl_init( 'https://yoursite.com/wp-json/wp/v2/posts?per_page=5' );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_HTTPHEADER, [ 'Accept: application/json' ] );
curl_setopt( $ch, CURLOPT_USERAGENT, 'MyApp/1.0' );
curl_setopt( $ch, CURLOPT_TIMEOUT, 15 );
$result = curl_exec( $ch );
curl_close( $ch );

$posts = json_decode( $result, true );
?>

Building Custom REST Routes

The most powerful feature of the WordPress REST API is the ability to register your own routes. This is ideal when you need endpoints tailored to your site's specific data, such as aggregated sales figures, filtered Custom Post Types, or computed results from your own business logic.

<?php
// Register custom routes on rest_api_init
add_action( 'rest_api_init', function() {

    // Public GET route
    register_rest_route( 'myplugin/v1', '/summary', array(
        'methods'             => 'GET',
        'callback'            => 'myplugin_get_summary',
        'permission_callback' => '__return_true',
    ) );

    // Authenticated POST route with argument validation
    register_rest_route( 'myplugin/v1', '/data', array(
        'methods'             => 'POST',
        'callback'            => 'myplugin_save_data',
        'permission_callback' => function() {
            return current_user_can( 'edit_posts' );
        },
        'args' => array(
            'title' => array(
                'required'          => true,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
            ),
            'amount' => array(
                'required' => true,
                'type'     => 'number',
            ),
        ),
    ) );
} );

// GET callback
function myplugin_get_summary( WP_REST_Request $request ) {
    return rest_ensure_response( array(
        'total_posts'  => wp_count_posts()->publish,
        'total_pages'  => wp_count_posts( 'page' )->publish,
        'total_users'  => count_users()['total_users'],
        'site_name'    => get_bloginfo( 'name' ),
        'generated_at' => current_time( 'mysql' ),
    ) );
}

// POST callback
function myplugin_save_data( WP_REST_Request $request ) {
    $title  = $request->get_param( 'title' );
    $amount = $request->get_param( 'amount' );

    // Process and store data...

    return new WP_REST_Response(
        array( 'success' => true, 'message' => 'Data saved successfully' ),
        201
    );
}
?>

After adding this code you can call https://yoursite.com/wp-json/myplugin/v1/summary immediately. Make sure WordPress Permalinks are not set to "Plain" — the rewrite rules required by /wp-json/ depend on a pretty-permalink structure.

Extending Standard Endpoints with Custom Fields

Sometimes you need to attach extra data to standard endpoint responses without replacing them. register_rest_field lets you inject additional properties into any existing resource type. The example below adds a featured_image_url field to every post response:

<?php
add_action( 'rest_api_init', function() {
    register_rest_field( 'post', 'featured_image_url', array(
        'get_callback' => function( $post_arr ) {
            $id  = get_post_thumbnail_id( $post_arr['id'] );
            if ( ! $id ) return null;
            $img = wp_get_attachment_image_src( $id, 'medium' );
            return $img ? $img[0] : null;
        },
        'schema' => array(
            'description' => 'Medium-size featured image URL',
            'type'        => 'string',
            'format'      => 'uri',
        ),
    ) );
} );
?>

With this in place, every /wp/v2/posts response will include featured_image_url, saving your frontend from making an additional request just to get the thumbnail.

Securing the WordPress REST API

WordPress exposes several public endpoints by default. While convenient for development, unmanaged access can introduce security risks. Here are the most important hardening steps:

1. Hide the Users Endpoint

By default, /wp-json/wp/v2/users exposes usernames to anyone, which can be harvested for brute-force attempts. Remove it for unauthenticated requests:

<?php
add_filter( 'rest_endpoints', function( $endpoints ) {
    if ( ! is_user_logged_in() ) {
        unset( $endpoints['/wp/v2/users'] );
        unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
    }
    return $endpoints;
} );
?>

2. Require Authentication for All Requests

If your site has no need to serve public API data, lock down the entire API for unauthenticated callers:

<?php
add_filter( 'rest_authentication_errors', function( $result ) {
    if ( true === $result || is_wp_error( $result ) ) return $result;
    if ( ! is_user_logged_in() ) {
        return new WP_Error(
            'rest_not_logged_in',
            'Authentication required to use this API.',
            array( 'status' => 401 )
        );
    }
    return $result;
} );
?>

3. Rate Limiting and Abuse Prevention

For publicly accessible APIs, implement rate limiting at the server or CDN layer. Cloudflare Rate Limiting rules are a convenient option — set a threshold of requests per IP per minute and return a 429 response for excess traffic. Combine this with server-side logging to detect unusual access patterns early.

Practical Example: Headless WordPress with React

The following example shows a minimal React component that fetches posts from the WordPress REST API, renders them as a list, and handles pagination using the response headers WordPress provides:

// BlogList.jsx
import { useState, useEffect } from 'react';

const WP_API = 'https://yoursite.com/wp-json/wp/v2';

export default function BlogList() {
  const [posts, setPosts]   = useState([]);
  const [loading, setLoading] = useState(true);
  const [page, setPage]     = useState(1);
  const [totalPages, setTotal] = useState(1);

  useEffect(() => {
    async function load() {
      setLoading(true);
      const res = await fetch(
        `${WP_API}/posts?per_page=10&page=${page}&_fields=id,title,slug,excerpt,date`
      );
      setTotal(parseInt(res.headers.get('X-WP-TotalPages') || '1', 10));
      setPosts(await res.json());
      setLoading(false);
    }
    load();
  }, [page]);

  if (loading) return <p>Loading...</p>;

  return (
    <div>
      {posts.map(p => (
        <article key={p.id}>
          <h2 dangerouslySetInnerHTML={{ __html: p.title.rendered }} />
          <div dangerouslySetInnerHTML={{ __html: p.excerpt.rendered }} />
          <a href={`/blog/${p.slug}`}>Read more →</a>
        </article>
      ))}
      <nav>
        <button onClick={() => setPage(n => n - 1)} disabled={page === 1}>Previous</button>
        <span> Page {page} of {totalPages} </span>
        <button onClick={() => setPage(n => n + 1)} disabled={page === totalPages}>Next</button>
      </nav>
    </div>
  );
}

WordPress automatically sends X-WP-Total (total items) and X-WP-TotalPages (total pages) response headers, making it straightforward to build fully functional pagination UI without any extra calculation on the client side.

Frequently Asked Questions (FAQ)

Is the WordPress REST API enabled by default?

Yes. The WordPress REST API has been enabled by default since WordPress 4.7 with no additional plugins required. You can verify it immediately by visiting https://yoursite.com/wp-json/wp/v2/posts in your browser. If you see a JSON array of posts the API is working. If not, check that your Permalinks are not set to Plain under Settings > Permalinks.

How do I restrict the WordPress REST API from public access?

The most common approach is to add a filter in functions.php using the rest_authentication_errors hook combined with is_user_logged_in(). This returns a 401 error for any unauthenticated request. For more granular control you can remove specific routes via the rest_endpoints filter, or require Application Password authentication on a per-route basis using the permission_callback parameter in register_rest_route.

What is the difference between WP_REST_Request and WP_REST_Response?

WP_REST_Request is the object that holds all incoming data from the client HTTP request including URL parameters, headers, and the request body. WP_REST_Response is the object you construct and return from your callback function to send data back to the client. You control the response data, HTTP status code, and headers through it. Both are always used together inside a custom route callback.

How does the WordPress REST API compare to GraphQL?

The REST API exposes separate HTTP endpoints per resource type such as /wp/v2/posts for posts and /wp/v2/users for users, and each response always includes every available field. GraphQL (via the WPGraphQL plugin for example) uses a single endpoint where the client declares exactly which fields it needs, eliminating over-fetching. For most standard projects and small headless setups the REST API is sufficient. GraphQL becomes more advantageous when you have a complex frontend that queries many different resource types with varying field requirements in a single round trip.

Hosting Optimised for WordPress

AsiaGB Hosting supports PHP 8.3, MySQL, LiteSpeed Cache, and the WordPress Toolkit — starting at 500 THB/year.

View Hosting Plans