How to Create a Custom REST API Endpoint in WordPress (Step by Step)

How to create a custom REST API endpoint in WordPress

The WordPress REST API lets other apps, JavaScript front ends and Gutenberg blocks read and write your site's data as JSON. WordPress ships with endpoints for posts, pages and users, but real projects almost always need a custom endpoint: a contact form handler, a stats feed for a mobile app, or data for a React-based block.

In this guide you'll build a custom endpoint from scratch, the way you would in a production plugin: with proper routing, argument validation, permission checks and clean responses.

What you'll build

Two routes under a custom namespace postverses/v1:

  • GET /wp-json/postverses/v1/books returns a list of books (public).
  • POST /wp-json/postverses/v1/books creates a book (only for users who can edit posts).

You can drop the code into a small plugin file such as wp-content/plugins/pv-books-api/pv-books-api.php. Avoid putting it in a theme's functions.php, because an API usually needs to survive theme changes.

Step 1: Register the route on rest_api_init

Routes must be registered inside the rest_api_init action with register_rest_route(). The first argument is your namespace (vendor name plus version), the second is the route.

<?php
/**
 * Plugin Name: PV Books API
 */

add_action( 'rest_api_init', function () {
    register_rest_route( 'postverses/v1', '/books', array(
        array(
            'methods'             => WP_REST_Server::READABLE,   // GET
            'callback'            => 'pv_get_books',
            'permission_callback' => '__return_true',            // public
            'args'                => array(
                'per_page' => array(
                    'default'           => 10,
                    'sanitize_callback' => 'absint',
                    'validate_callback' => function ( $value ) {
                        return $value > 0 && $value <= 50;
                    },
                ),
            ),
        ),
        array(
            'methods'             => WP_REST_Server::CREATABLE,  // POST
            'callback'            => 'pv_create_book',
            'permission_callback' => function () {
                return current_user_can( 'edit_posts' );
            },
            'args'                => array(
                'title'  => array(
                    'required'          => true,
                    'sanitize_callback' => 'sanitize_text_field',
                ),
                'author' => array(
                    'required'          => false,
                    'sanitize_callback' => 'sanitize_text_field',
                ),
            ),
        ),
    ) );
} );

Three details matter here:

  • permission_callback is required. Since WordPress 5.5, leaving it out triggers a _doing_it_wrong notice. For a public route, use __return_true explicitly so the intent is clear.
  • args validates input before your callback runs. If validate_callback returns false, WordPress answers with a 400 error automatically.
  • WP_REST_Server::READABLE and CREATABLE are constants for GET and POST; they read better than raw strings.

Step 2: Write the callbacks

A callback receives a WP_REST_Request object. Read parameters with $request->get_param() and return data with rest_ensure_response(), or return a WP_Error for failures.

function pv_get_books( WP_REST_Request $request ) {
    $query = new WP_Query( array(
        'post_type'      => 'book',
        'posts_per_page' => $request->get_param( 'per_page' ),
        'post_status'    => 'publish',
    ) );

    $books = array_map( function ( $post ) {
        return array(
            'id'     => $post->ID,
            'title'  => get_the_title( $post ),
            'author' => get_post_meta( $post->ID, 'book_author', true ),
            'link'   => get_permalink( $post ),
        );
    }, $query->posts );

    return rest_ensure_response( $books );
}

function pv_create_book( WP_REST_Request $request ) {
    $post_id = wp_insert_post( array(
        'post_type'   => 'book',
        'post_title'  => $request->get_param( 'title' ),
        'post_status' => 'publish',
    ), true );

    if ( is_wp_error( $post_id ) ) {
        return new WP_Error( 'pv_book_not_created', 'Could not create the book.', array( 'status' => 500 ) );
    }

    if ( $request->get_param( 'author' ) ) {
        update_post_meta( $post_id, 'book_author', $request->get_param( 'author' ) );
    }

    $response = rest_ensure_response( array( 'id' => $post_id ) );
    $response->set_status( 201 ); // Created
    return $response;
}

This example assumes a book custom post type exists. If you don't have one yet, register it with register_post_type() on the init hook, or simply change 'book' to 'post' while testing.

Step 3: Test the endpoint

After activating the plugin, visit https://your-site.com/wp-json/postverses/v1/books in a browser. You should see a JSON array. You can also test from the terminal:

curl https://your-site.com/wp-json/postverses/v1/books?per_page=5

Try an invalid value such as ?per_page=500 and you'll get a 400 rest_invalid_param response. That is your validation working.

Step 4: Authenticate write requests

The POST route checks current_user_can( 'edit_posts' ), so the request must come from a logged-in user. There are two common ways to authenticate:

  • Cookie + nonce (JavaScript running inside WordPress, for example a block or admin page). Create a nonce with wp_create_nonce( 'wp_rest' ), pass it to your script with wp_localize_script() or wp_add_inline_script(), and send it in the X-WP-Nonce header.
  • Application Passwords (external apps and scripts). Since WordPress 5.6 every user can create one under Users → Profile, then send it with HTTP Basic Auth over HTTPS.
// Inside WordPress (cookie + nonce)
fetch( '/wp-json/postverses/v1/books', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-WP-Nonce': pvApi.nonce
    },
    body: JSON.stringify( { title: 'Clean Code', author: 'Robert C. Martin' } )
} ).then( r => r.json() ).then( console.log );
# From an external script (Application Password)
curl -X POST https://your-site.com/wp-json/postverses/v1/books \
  -u "username:xxxx xxxx xxxx xxxx xxxx xxxx" \
  -H "Content-Type: application/json" \
  -d '{"title":"Clean Code","author":"Robert C. Martin"}'

Common mistakes to avoid

  • Returning raw WP_Post objects. They expose fields you don't intend to publish. Map them to a small array, as in the example.
  • Skipping sanitisation. Always set a sanitize_callback for every argument you store.
  • Using __return_true on write routes. Anyone could create content. Check a capability instead.
  • Getting 404 on /wp-json/. Re-save Settings → Permalinks; plain permalinks need ?rest_route=/postverses/v1/books instead.

Where to go next

For larger APIs, move the routes into a class that extends WP_REST_Controller; it gives you a standard structure for schemas, pagination headers and item permissions. If you are building blocks, the same endpoint can feed a Gutenberg block through @wordpress/api-fetch, which adds the nonce for you.

Related reading on Post Verses:

Post a Comment

Previous Post Next Post

Contact Form