Try it free for 30 daysStart free
TopSyde
Abstract artwork of a smoky quartz crystal lit with muted gold light on a dark background

Custom Endpoints and Auth for the WordPress REST API

Master WordPress REST API with authentication, custom endpoints, headless WordPress, and React/Next.js integration. Complete tutorial with real examples.

Marcus Webb

Marcus Webb

DevOps & Security Lead

··13 min read

Last updated: October 13, 2026

WordPress REST API provides a standardized way to interact with your WordPress site programmatically, enabling headless architectures, mobile apps, and third-party integrations through HTTP requests that return JSON responses.

What is the WordPress REST API?

The WordPress REST API is an interface that allows external applications to interact with WordPress sites using standard HTTP methods (GET, POST, PUT, DELETE). It transforms WordPress from a traditional CMS into a flexible content backend that can power websites, mobile apps, and headless architectures.

According to the 2024 WordPress Developer Survey, 34% of developers now use WordPress as a headless CMS, up from 22% in 2022. This growth reflects the API's maturity and the increasing demand for decoupled architectures.

The REST API follows REST (Representational State Transfer) principles, making it familiar to developers who already work with modern web APIs. Every WordPress installation since version 4.7 includes the REST API by default, accessible at /wp-json/wp/v2/ endpoints.

How WordPress REST API Authentication Works

WordPress REST API supports multiple authentication methods depending on your security requirements and use case. Understanding these methods matters for secure implementation.

Application Passwords

Application Passwords provide the simplest authentication method for trusted applications. Users generate unique passwords specifically for API access, separate from their regular login credentials.

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

JWT Token Authentication

JSON Web Tokens provide stateless authentication ideal for modern applications. JWT tokens contain encoded user information and expire after a set time, enhancing security.

OAuth 2.0 Integration

OAuth enables secure third-party application authorization without exposing user credentials. This method works best for applications that need user consent for data access.

WordPress REST API Endpoints Reference

WordPress provides numerous built-in endpoints for common operations. Understanding the endpoint structure helps you find your way around the API efficiently.

EndpointHTTP MethodPurposeAuthentication Required
/wp/v2/postsGETRetrieve postsNo
/wp/v2/postsPOSTCreate new postYes
/wp/v2/posts/{id}PUTUpdate existing postYes
/wp/v2/posts/{id}DELETEDelete postYes
/wp/v2/pagesGETRetrieve pagesNo
/wp/v2/usersGETList usersNo (limited data)
/wp/v2/mediaPOSTUpload mediaYes
/wp/v2/commentsGET/POSTManage commentsDepends on settings

Query Parameters and Filtering

The REST API supports extensive filtering and querying capabilities:

// Get 10 latest posts from category ID 5
fetch('/wp-json/wp/v2/posts?per_page=10&categories=5&orderby=date&order=desc')

// Search posts by keyword
fetch('/wp-json/wp/v2/posts?search=wordpress api')

// Get posts by specific author
fetch('/wp-json/wp/v2/posts?author=1')

How to Create Custom REST API Endpoints

Custom endpoints extend WordPress functionality beyond default content types. This enables specialized data handling and business logic implementation.

Basic Custom Endpoint Registration

function register_custom_endpoints() {
    register_rest_route('custom/v1', '/products', array(
        'methods' => 'GET',
        'callback' => 'get_custom_products',
        'permission_callback' => '__return_true'
    ));
}
add_action('rest_api_init', 'register_custom_endpoints');

function get_custom_products($request) {
    $products = get_posts(array(
        'post_type' => 'product',
        'numberposts' => $request->get_param('per_page') ?: 10
    ));
    
    $formatted_products = array_map(function($product) {
        return array(
            'id' => $product->ID,
            'title' => $product->post_title,
            'price' => get_post_meta($product->ID, '_price', true),
            'stock' => get_post_meta($product->ID, '_stock', true)
        );
    }, $products);
    
    return rest_ensure_response($formatted_products);
}

Advanced Endpoint with Validation

function register_advanced_endpoint() {
    register_rest_route('custom/v1', '/orders', array(
        'methods' => 'POST',
        'callback' => 'create_order',
        'permission_callback' => function() {
            return current_user_can('edit_posts');
        },
        'args' => array(
            'customer_email' => array(
                'required' => true,
                'validate_callback' => function($param) {
                    return is_email($param);
                }
            ),
            'items' => array(
                'required' => true,
                'validate_callback' => function($param) {
                    return is_array($param) && !empty($param);
                }
            )
        )
    ));
}

Headless WordPress with React and Next.js

Headless WordPress architectures separate content management from presentation, enabling modern frontend frameworks to consume WordPress data via the REST API.

React Integration Example

import React, { useState, useEffect } from 'react';

function WordPressPosts() {
    const [posts, setPosts] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        async function fetchPosts() {
            try {
                const response = await fetch('/wp-json/wp/v2/posts?per_page=6');
                const postsData = await response.json();
                setPosts(postsData);
            } catch (error) {
                console.error('Error fetching posts:', error);
            } finally {
                setLoading(false);
            }
        }

        fetchPosts();
    }, []);

    if (loading) return <div>Loading posts...</div>;

    return (
        <div className="posts-grid">
            {posts.map(post => (
                <article key={post.id} className="post-card">
                    <h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
                    <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
                    <a href={post.link}>Read More</a>
                </article>
            ))}
        </div>
    );
}

Next.js Static Generation

Next.js enables static site generation with WordPress data, combining the content management benefits of WordPress with the performance of static sites.

// pages/blog/[slug].js
export async function getStaticPaths() {
    const posts = await fetch('https://yoursite.com/wp-json/wp/v2/posts?per_page=100');
    const postsData = await posts.json();
    
    const paths = postsData.map(post => ({
        params: { slug: post.slug }
    }));

    return {
        paths,
        fallback: 'blocking'
    };
}

export async function getStaticProps({ params }) {
    const post = await fetch(`https://yoursite.com/wp-json/wp/v2/posts?slug=${params.slug}`);
    const postData = await post.json();

    return {
        props: {
            post: postData[0]
        },
        revalidate: 3600 // Revalidate every hour
    };
}

According to Jamstack Community Survey 2024, headless WordPress sites show 43% faster Time to First Byte (TTFB) compared to traditional WordPress themes when properly optimized.

WordPress REST API Performance Optimization

REST API performance directly impacts user experience, especially in headless architectures where every data request affects page load times.

Caching Strategies

Object caching significantly improves REST API response times. TopSyde's managed hosting includes Redis object caching optimized for REST API workloads.

function cache_api_response($endpoint, $data) {
    $cache_key = 'api_' . md5($endpoint . serialize($_GET));
    wp_cache_set($cache_key, $data, 'api_responses', 3600);
}

function get_cached_api_response($endpoint) {
    $cache_key = 'api_' . md5($endpoint . serialize($_GET));
    return wp_cache_get($cache_key, 'api_responses');
}

Database Query Optimization

Minimize database queries in custom endpoints by using efficient WordPress query methods:

function optimized_posts_endpoint($request) {
    // Use WP_Query for complex queries instead of multiple get_posts() calls
    $query_args = array(
        'post_type' => 'post',
        'posts_per_page' => $request->get_param('per_page') ?: 10,
        'meta_query' => array(
            array(
                'key' => '_featured',
                'value' => 'yes',
                'compare' => '='
            )
        )
    );
    
    $posts_query = new WP_Query($query_args);
    
    // Use get_field() for ACF fields to avoid individual meta queries
    $formatted_posts = array_map(function($post) {
        return array(
            'id' => $post->ID,
            'title' => $post->post_title,
            'featured_image' => get_field('featured_image', $post->ID),
            'excerpt' => wp_trim_words($post->post_content, 30)
        );
    }, $posts_query->posts);
    
    return rest_ensure_response($formatted_posts);
}

Response Field Selection

Limit API responses to necessary fields to reduce payload size and transfer time:

// Request only specific fields
fetch('/wp-json/wp/v2/posts?_fields=id,title,excerpt,date,link')

// Embed related data in single request
fetch('/wp-json/wp/v2/posts?_embed=author,wp:featuredmedia')

WordPress REST API Security Best Practices

REST API security requires multiple layers of protection, from authentication to input validation and rate limiting.

Rate Limiting Implementation

Prevent API abuse with rate limiting based on IP address or user authentication:

function implement_api_rate_limiting() {
    $ip = $_SERVER['REMOTE_ADDR'];
    $rate_limit_key = 'api_rate_limit_' . md5($ip);
    
    $current_requests = get_transient($rate_limit_key) ?: 0;
    
    if ($current_requests >= 100) { // 100 requests per hour
        wp_die('Rate limit exceeded', 'Too Many Requests', array('response' => 429));
    }
    
    set_transient($rate_limit_key, $current_requests + 1, 3600);
}
add_action('rest_api_init', 'implement_api_rate_limiting');

Input Validation and Sanitization

Validate all input data to prevent injection attacks and data corruption:

function validate_custom_endpoint_data($request) {
    $email = sanitize_email($request->get_param('email'));
    if (!is_email($email)) {
        return new WP_Error('invalid_email', 'Invalid email format', array('status' => 400));
    }
    
    $content = wp_kses_post($request->get_param('content'));
    if (empty($content)) {
        return new WP_Error('empty_content', 'Content cannot be empty', array('status' => 400));
    }
    
    return true;
}

HTTPS and CORS Configuration

Always use HTTPS for API endpoints handling sensitive data. Configure CORS headers for cross-origin requests:

function configure_api_cors() {
    header('Access-Control-Allow-Origin: https://yourdomain.com');
    header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
    header('Access-Control-Allow-Headers: Authorization, Content-Type');
}
add_action('rest_api_init', 'configure_api_cors');

For comprehensive WordPress security practices, review our WordPress Security Best Practices guide.

Real-World WordPress REST API Use Cases

E-commerce Integration

WooCommerce REST API extends WordPress capabilities for online stores:

// Retrieve products with pricing and inventory
async function getProducts(category = null) {
    let url = '/wp-json/wc/v3/products';
    if (category) {
        url += `?category=${category}`;
    }
    
    const response = await fetch(url, {
        headers: {
            'Authorization': 'Basic ' + btoa('consumer_key:consumer_secret')
        }
    });
    
    return await response.json();
}

// Create new order
async function createOrder(orderData) {
    const response = await fetch('/wp-json/wc/v3/orders', {
        method: 'POST',
        headers: {
            'Authorization': 'Basic ' + btoa('consumer_key:consumer_secret'),
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(orderData)
    });
    
    return await response.json();
}

Mobile App Backend

WordPress REST API works well as a backend for mobile applications:

// Mobile app authentication flow
class WordPressAuth {
    async login(username, password) {
        const response = await fetch('/wp-json/jwt-auth/v1/token', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ username, password })
        });
        
        const data = await response.json();
        if (data.token) {
            await AsyncStorage.setItem('jwt_token', data.token);
            return true;
        }
        return false;
    }
    
    async getProfile() {
        const token = await AsyncStorage.getItem('jwt_token');
        const response = await fetch('/wp-json/wp/v2/users/me', {
            headers: { 'Authorization': `Bearer ${token}` }
        });
        
        return await response.json();
    }
}

Multi-site Content Syndication

REST API enables content sharing across multiple WordPress installations:

function sync_content_across_sites($post_id) {
    $post = get_post($post_id);
    $sites = ['https://site1.com', 'https://site2.com'];
    
    // Remote post IDs from earlier syncs, so updates don't create duplicates
    $remote_ids = get_post_meta($post_id, '_syndicated_ids', true) ?: array();
    
    foreach ($sites as $site) {
        $endpoint = $site . '/wp-json/wp/v2/posts';
        if (!empty($remote_ids[$site])) {
            $endpoint .= '/' . (int) $remote_ids[$site]; // update the existing copy
        }
        
        $response = wp_remote_post($endpoint, array(
            'headers' => array(
                'Authorization' => 'Basic ' . base64_encode('username:application_password'),
                'Content-Type' => 'application/json',
            ),
            'body' => wp_json_encode(array(
                'title'   => $post->post_title,
                'content' => $post->post_content,
                'excerpt' => $post->post_excerpt,
                'status'  => 'publish',
            )),
            'timeout' => 15,
        ));
        
        if (is_wp_error($response)) {
            error_log('Syndication to ' . $site . ' failed: ' . $response->get_error_message());
            continue;
        }
        
        $body = json_decode(wp_remote_retrieve_body($response), true);
        if (!empty($body['id'])) {
            $remote_ids[$site] = (int) $body['id'];
        }
    }
    
    update_post_meta($post_id, '_syndicated_ids', $remote_ids);
}
add_action('publish_post', 'sync_content_across_sites');

The publish_post hook fires on the first publish and again on every update to a published post, which is why the function stores each remote ID and sends later saves to /posts/{id} instead of creating a new copy. Each receiving site needs a dedicated user with an Application Password and only the role it needs (Author is usually enough). Keep those credentials in wp-config.php constants rather than in theme code.

Troubleshooting Common REST API Errors

Most REST API problems come down to a handful of error codes. The JSON body of the error response names the code, so read it before changing anything.

ErrorWhat it usually meansFix
401 rest_not_logged_in or rest_cannot_createNo credentials reached WordPressCheck that the server passes the Authorization header through to PHP
401 incorrect_passwordWrong username or Application PasswordUse the WordPress username, not the email, and the generated password
403 rest_cookie_invalid_nonceCookie auth without a valid nonceSend the X-WP-Nonce header from wp_create_nonce('wp_rest')
403 rest_forbidden or rest_cannot_createAuthenticated user lacks the capabilityGive the API user the right role, or check your permission_callback
404 rest_no_routeRoute not registered or permalinks brokenConfirm register_rest_route() runs on rest_api_init, then resave permalinks

Three causes come up again and again:

  • The Authorization header is stripped. Some Apache and CGI setups drop it before PHP sees it. Adding SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 to .htaccess usually fixes it. NGINX with PHP-FPM passes the header through by default.
  • Application Passwords are unavailable. WordPress only offers them over HTTPS (local development environments excepted), and some security plugins switch them off. If the section is missing from the user profile screen, check both.
  • A security plugin blocks the API. Plugins that "disable the REST API" for logged-out visitors break headless frontends, contact forms and the block editor's previews. Restrict specific routes instead, as described in our rate limiting and DDoS guide.

Turn on WP_DEBUG_LOG while you debug custom endpoints. A PHP warning printed before the JSON corrupts the response and shows up in JavaScript as "Unexpected token in JSON", which hides the real error. Our WordPress error debugging guide covers the logging setup.

Frequently Asked Questions

Is the WordPress REST API enabled by default?

Yes. Every WordPress install since 4.7 serves the REST API at /wp-json/, and the block editor depends on it, so it can't be switched off without breaking the admin. Public content such as published posts and pages is readable without logging in, while anything that changes data needs authentication.

Should I disable the REST API for security?

No. Disabling it breaks the block editor, many plugins, and any headless or mobile frontend. The safer approach is to restrict the routes that leak information, such as the users endpoint for logged-out visitors, add rate limiting at the server, and make sure every custom endpoint has a real permission_callback instead of __return_true for anything that writes data.

What is the difference between Application Passwords and JWT authentication?

Application Passwords are built into WordPress core, need no plugin, and send a revocable per-app password with each request over HTTPS. JWT requires a plugin and issues a short-lived token after a username and password login, which suits mobile apps and frontends where users sign in themselves. For server-to-server integrations, Application Passwords are the simpler and better-supported choice.

How do I add custom fields to REST API responses?

For post meta, register the field with register_post_meta() and set show_in_rest to true, which also makes it writable through the API with the right permissions. For computed values or data from other tables, use register_rest_field() with a get_callback. ACF field groups have their own "Show in REST API" setting that exposes fields under an acf key.

Is headless WordPress always faster than a traditional theme?

Not automatically. A statically generated or cached Next.js frontend can be very fast, but you take on a second application to build, host and maintain, and features like previews, forms and SEO plugins need extra work. A well-built traditional theme on fast hosting with page caching is often fast enough. Go headless when you need the frontend flexibility, not only for speed.

Keep Reading on TopSyde

Marcus Webb
Marcus Webb

DevOps & Security Lead

12+ years DevOps, Linux & cloud infrastructure certified

Marcus leads infrastructure and security at TopSyde, managing the server fleet and AI monitoring systems that keep client sites fast and protected. Former sysadmin turned WordPress hosting specialist.

Related Articles

View all →

Free AI audit

Is your site actually fast?

Get a free homepage audit covering performance, SEO and usability, with a prioritized fix list. See the report on screen and get a copy by email. No account or payment required. Allow a few minutes for the checks.

Free homepage report. No account or payment required.