How to Register a Custom Endpoint with the WordPress REST API

The WordPress REST API, introduced in WordPress 4.7, lets you expose your data to JavaScript front-ends, mobile apps, and external services over HTTP. Registering a custom endpoint takes a handful of lines and works alongside all existing authentication and permission checks.

Problem: How do you register a custom route in the WordPress REST API that isn't tied to a built-in post type?

Solution: Use register_rest_route() inside a rest_api_init action hook to define your endpoint, its HTTP methods, callback, and permission callback.

The example below registers a public read endpoint and an authenticated write endpoint under the /wp-json/myplugin/v1/ namespace:

add_action( 'rest_api_init', 'register_my_rest_routes' );

function register_my_rest_routes() {

    // Public GET endpoint
    register_rest_route( 'myplugin/v1', '/posts/', [
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'get_my_posts',
        'permission_callback' => '__return_true', // public
        'args'                => [
            'count' => [
                'default'           => 5,
                'sanitize_callback' => 'absint',
                'validate_callback' => function( $val ) {
                    return $val > 0 && $val <= 50;
                },
            ],
        ],
    ] );

    // Authenticated POST endpoint
    register_rest_route( 'myplugin/v1', '/note/', [
        'methods'             => WP_REST_Server::CREATABLE,
        'callback'            => 'save_my_note',
        'permission_callback' => function() {
            return current_user_can( 'edit_posts' );
        },
    ] );
}

function get_my_posts( WP_REST_Request $request ) {
    $posts = get_posts( [
        'posts_per_page' => $request->get_param( 'count' ),
        'post_status'    => 'publish',
    ] );

    $data = array_map( function( $post ) {
        return [
            'id'    => $post->ID,
            'title' => $post->post_title,
            'link'  => get_permalink( $post ),
        ];
    }, $posts );

    return rest_ensure_response( $data );
}

function save_my_note( WP_REST_Request $request ) {
    $note = sanitize_textarea_field( $request->get_param( 'note' ) );
    update_option( 'my_saved_note', $note );
    return rest_ensure_response( [ 'saved' => true ] );
}

NOTE: Never set permission_callback to __return_true on a write endpoint. Always check capabilities. Return errors as WP_Error objects — the REST API will convert them to proper JSON error responses with HTTP status codes.