Skip to main content
Exposing Curated Data: Building Authenticated Custom REST API Endpoints in WordPress

Exposing Curated Data: Building Authenticated Custom REST API Endpoints in WordPress

As WordPress evolves into a powerful application framework, the need to expose specific, curated data programmatically, often beyond what the default REST API…

As WordPress evolves into a powerful application framework, the need to expose specific, curated data programmatically, often beyond what the default REST API offers, becomes increasingly common. Perhaps you’re integrating with a custom mobile app, a third-party analytics dashboard, or another microservice that requires a tailored data schema or specific authentication rules. In these scenarios, designing your own custom REST endpoints is a core skill for any advanced WordPress developer. I’ve found that this approach grants immense control over data presentation and security, which is vital for robust backend infrastructure.

My typical approach begins with registering the custom route. This is done using the register_rest_route function, ideally within an add_action hook tied to rest_api_init. It allows us to define the namespace (critical for avoiding conflicts), the specific endpoint route, the HTTP methods it supports (GET, POST, etc.), and the crucial callback and permission functions. I also leverage the args parameter to define expected query parameters, complete with sanitation and validation callbacks to ensure data integrity from the start.

Here’s an example of registering a GET endpoint for retrieving product reviews, including a min_rating parameter for filtering:

add_action( 'rest_api_init', function () {
        register_rest_route( 'my-api/v1', '/reviews', array(
            'methods'             => 'GET',
            'callback'            => 'my_api_get_product_reviews',
            'permission_callback' => 'my_api_product_reviews_permissions_check',
            'args'                => array(
                'min_rating' => array(
                    'sanitize_callback' => 'absint',
                    'validate_callback' => function( $param, $request, $key ) {
                        return is_numeric( $param ) && $param >= 1 && $param <= 5;
                    },
                    'required'    => false,
                    'description' => 'Minimum rating for reviews (1-5).'
                ),
            ),
        ) );
    } );

The real work happens in the callback function. This is where I query WordPress data, transform it into the desired JSON structure, and return it. For our product reviews example, I’d typically query a custom post type product_review, fetch its title, content, and any custom fields like rating and reviewer_name. It’s important to be efficient; for instance, fetching only post IDs first and then iterating to get specific fields can sometimes be more performant than a single, complex WP_Query if you need to build a heavily customized object. Filtering logic, based on the parameters passed to the endpoint, also lives here.

function my_api_get_product_reviews( $request ) {
        $args = array(
            'post_type'      => 'product_review',
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'fields'         => 'ids',
        );

$min_rating = $request->get_param( 'min_rating' );
        if ( $min_rating ) {
            $args['meta_query'] = array(
                array(
                    'key'     => 'rating',
                    'value'   => $min_rating,
                    'type'    => 'NUMERIC',
                    'compare' => '>=',
                ),
            );
        }

$query = new WP_Query( $args );
        $reviews = array();

if ( $query->have_posts() ) {
            foreach ( $query->posts as $post_id ) {
                $reviews[] = array(
                    'id'            => $post_id,
                    'title'         => get_the_title( $post_id ),
                    'content'       => get_post_field( 'post_content', $post_id ),
                    'rating'        => (int) get_post_meta( $post_id, 'rating', true ),
                    'reviewer_name' => get_post_meta( $post_id, 'reviewer_name', true ),
                    'date'          => get_the_date( 'c', $post_id ),
                );
            }
        }

return new WP_REST_Response( $reviews, 200 );
    }

Security is paramount, and the permission_callback is your gatekeeper. For simple internal tools or applications where the user is already logged into WordPress, leveraging current_user_can() is an effective method. This checks if the current user (identified by their session cookies) has a specific capability. If the user isn’t logged in or doesn’t have the required capability, I return a WP_Error with an appropriate status code, typically 401 or 403. For truly external, stateless applications requiring API key or token-based authentication, you’d typically implement custom logic here or integrate with a plugin like WP REST API – JWT Authentication, or leverage WordPress’s built-in Application Passwords feature, but for many use cases, checking current_user_can is sufficient.

function my_api_product_reviews_permissions_check( $request ) {
        if ( current_user_can( 'edit_posts' ) ) {
            return true;
        }
        return new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to access this resource.', 'my-plugin' ), array( 'status' => 401 ) );
    }

Once implemented (typically within a custom plugin or your theme’s functions.php), this endpoint becomes accessible at yourdomain.com/wp-json/my-api/v1/reviews. For logged-in users with edit_posts capability, they could access /wp-json/my-api/v1/reviews?min_rating=4 to get reviews meeting the criteria. This modular approach ensures your custom data is exposed precisely how and to whom you intend, forming a solid foundation for complex WordPress-powered applications.