Building a WordPress REST API plugin is the fastest way to turn a WordPress site into something more than a content management system. Whether you need to feed data to a mobile app, connect a headless frontend, or integrate with external services like CRMs and AI platforms, the REST API is how modern WordPress development gets done.

This guide walks through the full process of wordpress rest api plugin development – from registering custom endpoints to handling authentication, validation, and error responses. Every code example is production-ready. If you are a developer looking to build API-driven plugins or an agency evaluating what is possible, this covers the technical ground you need.

Why the WordPress REST API Matters

WordPress powers over 40% of the web. But the traditional approach – PHP templates rendering HTML on the server – limits what you can build. The REST API removes that limitation entirely.

With the REST API, WordPress becomes a structured data backend. Your site exposes JSON endpoints that any application can consume. The content stays in WordPress. The presentation layer can be anything.

This unlocks three major patterns:

  • Headless WordPress: use React, Next.js, or Vue for the frontend while WordPress handles content and data
  • Mobile applications: native iOS and Android apps that read and write to your WordPress database
  • Third-party integrations: connect WordPress to CRMs, payment processors, analytics platforms, and AI services without building custom middleware

For wordpress api development, this is not theoretical. It is how production systems work today. Every time an agency client asks for a dashboard that pulls data from their WordPress site, or a mobile app that syncs with their WooCommerce store, the REST API is the answer.

What Comes Built In

WordPress ships with a complete set of default REST API endpoints out of the box:

  • /wp-json/wp/v2/posts – create, read, update, delete posts
  • /wp-json/wp/v2/pages – same for pages
  • /wp-json/wp/v2/users – user management
  • /wp-json/wp/v2/media – media library access
  • /wp-json/wp/v2/comments – comment operations
  • /wp-json/wc/v3/* – WooCommerce endpoints (if WooCommerce is active)

These defaults handle standard content operations. But the real power comes when you register your own.

Registering Custom REST API Endpoints

The core of any wordpress rest api plugin is register_rest_route(). This function tells WordPress to listen for requests at a specific URL pattern and hand them off to your callback function.

Here is a complete example that registers a custom endpoint:

<?php
/**
 * Plugin Name: Custom API Endpoints
 * Description: Registers custom REST API endpoints for the application.
 * Version: 1.0.0
 */

add_action( 'rest_api_init', 'cae_register_routes' );

function cae_register_routes() {
    register_rest_route( 'custom-api/v1', '/leads', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'cae_get_leads',
        'permission_callback' => 'cae_check_admin_permission',
    ) );

    register_rest_route( 'custom-api/v1', '/leads', array(
        'methods'             => WP_REST_Server::CREATABLE,
        'callback'            => 'cae_create_lead',
        'permission_callback' => 'cae_check_admin_permission',
        'args'                => cae_get_lead_schema(),
    ) );
}

Key details:

  • Namespace (custom-api/v1) – keeps your routes separate from core and other plugins. Always include a version number.
  • Methods: use the WP_REST_Server constants: READABLE (GET), CREATABLE (POST), EDITABLE (PUT/PATCH), DELETABLE (DELETE).
  • Callback: the function that processes the request and returns data.
  • Permission callback: runs before your callback. If it returns false, WordPress sends a 403 response automatically.

Route Parameters

You can define dynamic URL segments using regex patterns:

register_rest_route( 'custom-api/v1', '/leads/(?P<id>d+)', array(
    'methods'             => WP_REST_Server::READABLE,
    'callback'            => 'cae_get_single_lead',
    'permission_callback' => 'cae_check_admin_permission',
    'args'                => array(
        'id' => array(
            'required'          => true,
            'validate_callback' => function( $param ) {
                return is_numeric( $param ) && $param > 0;
            },
            'sanitize_callback' => 'absint',
        ),
    ),
) );

The (?P<id>d+) pattern captures a numeric ID from the URL. WordPress passes it to your callback through the $request object.

Authentication and Permissions

Every wordpress rest api custom endpoint needs a permission callback. Skipping this – or returning true unconditionally – is a security hole.

Permission Callback Patterns

Admin-only access:

function cae_check_admin_permission( $request ) {
    return current_user_can( 'manage_options' );
}

Authenticated users only:

function cae_check_authenticated( $request ) {
    return is_user_logged_in();
}

Public read, authenticated write:

// For GET requests - public
register_rest_route( 'custom-api/v1', '/catalogue', array(
    'methods'             => WP_REST_Server::READABLE,
    'callback'            => 'cae_get_catalogue',
    'permission_callback' => '__return_true',
) );

// For POST requests - authenticated
register_rest_route( 'custom-api/v1', '/catalogue', array(
    'methods'             => WP_REST_Server::CREATABLE,
    'callback'            => 'cae_create_catalogue_item',
    'permission_callback' => function() {
        return current_user_can( 'edit_posts' );
    },
) );

Authentication Methods

WordPress supports several authentication approaches for REST API requests:

Method Best For Security Level
Cookie + Nonce Same-domain JS (wp-admin, frontend scripts) High (CSRF protected)
Application Passwords External apps, mobile clients, server-to-server High
OAuth 2.0 Third-party integrations with user consent Highest
JWT (via plugin) Single-page apps, headless frontends High

Application Passwords (built into WordPress since 5.6) are the simplest option for external integrations. Each user can generate multiple passwords with different labels, and they can be revoked individually without changing the main password.

Data Validation and Sanitisation

Never trust incoming data. The REST API provides built-in mechanisms for validation and sanitisation that run before your callback ever executes.

Schema-Based Validation

Define your expected data structure using JSON Schema:

function cae_get_lead_schema() {
    return array(
        'email' => array(
            'required'          => true,
            'type'              => 'string',
            'format'            => 'email',
            'sanitize_callback' => 'sanitize_email',
            'description'       => 'Contact email address.',
        ),
        'name' => array(
            'required'          => true,
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'description'       => 'Full name of the lead.',
        ),
        'source' => array(
            'required'          => false,
            'type'              => 'string',
            'default'           => 'website',
            'enum'              => array( 'website', 'referral', 'social', 'paid' ),
            'sanitize_callback' => 'sanitize_text_field',
            'description'       => 'Lead acquisition source.',
        ),
        'notes' => array(
            'required'          => false,
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_textarea_field',
            'description'       => 'Additional notes about the lead.',
        ),
    );
}

With this schema in place, WordPress automatically rejects requests with missing required fields, wrong data types, or values not in the enum list. Your callback only receives clean, validated data.

The Callback Function

Here is a complete callback that creates a lead and returns a proper REST response:

function cae_create_lead( WP_REST_Request $request ) {
    $lead_data = array(
        'post_title'  => $request->get_param( 'name' ),
        'post_type'   => 'lead',
        'post_status' => 'publish',
        'meta_input'  => array(
            '_lead_email'  => $request->get_param( 'email' ),
            '_lead_source' => $request->get_param( 'source' ),
            '_lead_notes'  => $request->get_param( 'notes' ),
        ),
    );

    $lead_id = wp_insert_post( $lead_data, true );

    if ( is_wp_error( $lead_id ) ) {
        return new WP_REST_Response( array(
            'code'    => 'create_failed',
            'message' => $lead_id->get_error_message(),
        ), 500 );
    }

    $response_data = array(
        'id'     => $lead_id,
        'name'   => $request->get_param( 'name' ),
        'email'  => $request->get_param( 'email' ),
        'source' => $request->get_param( 'source' ),
    );

    return new WP_REST_Response( $response_data, 201 );
}

Key practices:

  • Use WP_REST_Request type hint for the request parameter
  • Return WP_REST_Response objects with appropriate HTTP status codes (201 for created, 200 for success, 4xx/5xx for errors)
  • Use wp_insert_post with true as the second parameter to get WP_Error objects on failure
  • Never echo output – always return response objects

Structuring a Production Plugin

A real wordpress rest api plugin needs more structure than a single file. Here is a recommended layout:

my-api-plugin/
    my-api-plugin.php          # Main plugin file, hooks, activation
    includes/
        class-rest-controller.php   # Route registration
        class-lead-controller.php   # Lead-specific endpoints
        class-auth-handler.php      # Custom auth logic
    assets/
        js/
        css/
    readme.txt

Using a Controller Class

The WordPress REST API is designed to work with controller classes that extend WP_REST_Controller:

class Lead_REST_Controller extends WP_REST_Controller {

    protected $namespace = 'custom-api/v1';
    protected $rest_base = 'leads';

    public function register_routes() {
        register_rest_route( $this->namespace, '/' . $this->rest_base, array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_items' ),
                'permission_callback' => array( $this, 'get_items_permissions_check' ),
                'args'                => $this->get_collection_params(),
            ),
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array( $this, 'create_item' ),
                'permission_callback' => array( $this, 'create_item_permissions_check' ),
                'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
            ),
            'schema' => array( $this, 'get_public_item_schema' ),
        ) );
    }

    public function get_items_permissions_check( $request ) {
        return current_user_can( 'edit_posts' );
    }

    public function create_item_permissions_check( $request ) {
        return current_user_can( 'manage_options' );
    }

    // ... get_items(), create_item(), get_item_schema() methods
}

This pattern gives you consistent method naming, built-in schema support, and cleaner code organisation across multiple endpoint groups.

Real-World Use Cases

The wordpress plugin rest api pattern is not academic. Here are concrete scenarios where custom endpoints solve real business problems.

CRM Integration

A lead management system needs to sync data between WordPress and external platforms. Custom endpoints handle incoming webhooks from CRMs, push new lead data to third-party services, and provide dashboards with real-time data.

This is exactly what Leadio does – it exposes REST API endpoints that connect WordPress with Cal.com for scheduling and Fathom Analytics for tracking, creating a unified lead management CRM that lives inside WordPress rather than requiring a separate SaaS platform.

AI Service Integration

Connecting WordPress to AI services – OpenAI, Anthropic, Google Gemini – requires custom endpoints that act as secure proxies. The frontend sends a request to your WordPress endpoint, which adds API keys server-side and forwards the request to the AI service. The response comes back through your endpoint, where you can log usage, cache results, and enforce rate limits.

AI2WP uses this pattern to let users generate content with AI models and publish it directly as WordPress pages. The REST API handles the entire flow – sending prompts, receiving completions, and creating posts – without exposing any API credentials to the browser.

Headless WordPress for Agencies

Agency clients increasingly want custom frontends built with React or Next.js while keeping WordPress as the content backend. Custom REST API endpoints let you shape the data exactly as the frontend needs it – combining posts, custom fields, taxonomy data, and computed values into a single response instead of forcing the frontend to make multiple requests.

For more on building custom solutions for agency clients, see my guide on custom WordPress plugin development for agencies.

Mobile App Backend

A WordPress site with WooCommerce can serve as the complete backend for a mobile shopping app. Custom endpoints handle:

  • Product search with custom filtering and sorting logic
  • Cart management tied to user sessions
  • Order placement with payment gateway integration
  • Push notification triggers on order status changes

External Dashboard and Reporting

Custom endpoints can aggregate data from WordPress, WooCommerce, and connected services into a single API response that feeds an external dashboard or reporting tool. Instead of querying multiple databases and APIs from the dashboard, you build one WordPress endpoint that does the heavy lifting.

Error Handling Best Practices

Proper error responses make your API usable. Poor error responses make debugging a nightmare.

Use WP_Error Consistently

function cae_get_single_lead( WP_REST_Request $request ) {
    $lead_id = $request->get_param( 'id' );
    $lead    = get_post( $lead_id );

    if ( ! $lead || 'lead' !== $lead->post_type ) {
        return new WP_Error(
            'lead_not_found',
            'No lead found with the specified ID.',
            array( 'status' => 404 )
        );
    }

    if ( 'trash' === $lead->post_status ) {
        return new WP_Error(
            'lead_deleted',
            'This lead has been deleted.',
            array( 'status' => 410 )
        );
    }

    return new WP_REST_Response( cae_format_lead( $lead ), 200 );
}

Error handling rules:

  • Always return WP_Error for error cases, not false or empty arrays
  • Include a machine-readable error code (e.g., lead_not_found)
  • Include a human-readable message
  • Set the correct HTTP status code in the data array
  • Use standard HTTP codes: 400 (bad request), 401 (unauthorised), 403 (forbidden), 404 (not found), 500 (server error)

Performance Considerations

REST API endpoints that run on every request need to be efficient.

  • Use transients for caching: if your endpoint aggregates data that does not change every second, cache the result with set_transient() and serve it until it expires
  • Paginate results: never return unbounded lists. Use per_page and page parameters with appropriate defaults and maximums
  • Select only what you need: use WP_Query with specific fields and meta_query parameters rather than pulling entire post objects
  • Rate limit external calls: if your endpoint calls a third-party API, implement rate limiting to avoid hitting their limits and slowing down your response
// Paginated endpoint example
function cae_get_leads( WP_REST_Request $request ) {
    $per_page = $request->get_param( 'per_page' ) ?: 10;
    $page     = $request->get_param( 'page' ) ?: 1;

    $query = new WP_Query( array(
        'post_type'      => 'lead',
        'posts_per_page' => min( $per_page, 100 ),
        'paged'          => $page,
        'orderby'        => 'date',
        'order'          => 'DESC',
    ) );

    $leads = array_map( 'cae_format_lead', $query->posts );

    $response = new WP_REST_Response( $leads, 200 );
    $response->header( 'X-WP-Total', $query->found_posts );
    $response->header( 'X-WP-TotalPages', $query->max_num_pages );

    return $response;
}

Testing Your Endpoints

Before deploying, test your endpoints thoroughly.

Using cURL

# GET request
curl -X GET https://yoursite.com/wp-json/custom-api/v1/leads 
  -H "Authorization: Basic $(echo -n 'username:app_password' | base64)"

# POST request with data
curl -X POST https://yoursite.com/wp-json/custom-api/v1/leads 
  -H "Authorization: Basic $(echo -n 'username:app_password' | base64)" 
  -H "Content-Type: application/json" 
  -d '{"name": "Jane Smith", "email": "jane@example.com", "source": "referral"}'

Using Postman or Hoppscotch

GUI tools like Postman are useful for exploring your API interactively. Set up an environment with your base URL and authentication credentials, then build a collection of requests for each endpoint.

Automated Testing

For production plugins, write PHPUnit tests that exercise your endpoints:

class Test_Lead_Endpoint extends WP_Test_REST_Controller_Testcase {

    public function test_create_lead_returns_201() {
        $this->set_current_user_to_admin();

        $request = new WP_REST_Request( 'POST', '/custom-api/v1/leads' );
        $request->set_body_params( array(
            'name'  => 'Test Lead',
            'email' => 'test@example.com',
        ) );

        $response = rest_get_server()->dispatch( $request );
        $this->assertEquals( 201, $response->get_status() );
    }

    public function test_unauthenticated_request_returns_403() {
        wp_set_current_user( 0 );

        $request  = new WP_REST_Request( 'GET', '/custom-api/v1/leads' );
        $response = rest_get_server()->dispatch( $request );
        $this->assertEquals( 403, $response->get_status() );
    }
}

When to Build Custom vs Use Existing Plugins

Not every project needs custom endpoints. Here is a decision framework:

Build custom when:

  • You need endpoints that shape data for a specific frontend or app
  • The integration requires business logic that no existing plugin handles
  • Security requirements demand full control over authentication and data access
  • Performance needs require optimised queries rather than generic plugin overhead

Use existing plugins when:

  • Standard CRUD operations on default WordPress content types are sufficient
  • A well-maintained plugin already exposes the exact endpoints you need
  • Budget and timeline do not justify custom development

For a deeper look at building WordPress plugins from scratch, our how to build a WordPress plugin guide covers the fundamentals.

Need API-driven WordPress development? Whether you are building a headless frontend, connecting to external services, or need a custom plugin with REST API endpoints, devash.pro builds WordPress solutions that integrate with anything. From Leadio’s CRM integrations to AI2WP’s AI service connections – I build plugins that talk to the systems your business runs on. Get in touch to discuss your project.