← All posts

Crafting Secure, Custom WordPress REST Endpoints for Project Data

Crafting Secure, Custom WordPress REST Endpoints for Project Data

When extending WordPress beyond a traditional CMS, its built-in REST API is a powerful foundation. However, the default endpoints often don't provide the granular control or custom data structures required by modern applications. Fetching specific datasets, integrating with internal tools, or exposing unique business logic frequently calls for creating your own, highly tailored REST endpoints. Today, I want to walk through how we can design a secure, custom endpoint to expose specific data from a custom post type, complete with authentication.

Imagine we have a WordPress site managing "Projects" as a custom post type, each with custom fields like `_project_status` (e.g., 'active', 'completed', 'on-hold') and `_project_deadline`. Our goal is to create a secure endpoint that exclusively returns *active* projects to an internal dashboard application, formatted precisely for its needs, and protected so only authorized systems can access it. The default `/wp/v2/projects` endpoint might expose too much data, or require extensive filtering on the client side, and lack custom authentication mechanisms needed for system-to-system integration.

The first step is registering our custom route. We'll use the `register_rest_route` function, typically within a plugin or a theme's functions.php, ensuring it fires on the `rest_api_init` action. This function takes an API namespace, a route string, and an array of arguments defining the endpoint's behavior.

add_action( 'rest_api_init', function () { register_rest_route( 'my-projects/v1', '/active', array( 'methods' => 'GET', 'callback' => 'my_projects_get_active_projects', 'permission_callback' => 'my_projects_check_api_key', ) ); } );

Next, let's implement the `my_projects_get_active_projects` callback function. This function will handle the actual data retrieval and formatting. Inside, we'll use `WP_Query` to fetch our custom post type posts, specifically filtering for `_project_status` set to 'active'. We'll then loop through the results, extracting and structuring only the necessary data points, ensuring a lean and focused response tailored for our dashboard.

function my_projects_get_active_projects( $request ) { $args = array( 'post_type' => 'project', 'posts_per_page' => -1, // Get all active projects 'meta_query' => array( array( 'key' => '_project_status', 'value' => 'active', 'compare' => '=', ), ), );

$projects_query = new WP_Query( $args ); $projects_data = array();

if ( $projects_query->have_posts() ) { while ( $projects_query->have_posts() ) { $projects_query->the_post(); $project_id = get_the_ID(); $projects_data[] = array( 'id' => $project_id, 'title' => get_the_title(), 'status' => get_post_meta( $project_id, '_project_status', true ), 'deadline' => get_post_meta( $project_id, '_project_deadline', true ), 'link' => get_permalink( $project_id ), ); } wp_reset_postdata(); }

return new WP_REST_Response( $projects_data, 200 ); }

Finally, for security, we implement the `my_projects_check_api_key` permission callback. For a simple system-to-system integration, a custom API key (a bearer token) passed in the `Authorization` header is often sufficient. We'll assume this key is stored securely as an option in our database. While OAuth 2.0 or JWTs offer more robust solutions for complex scenarios, a simple bearer token provides a clear, demonstrable example here.

function my_projects_check_api_key( $request ) { $api_key = get_option( 'my_projects_api_key' ); // Stored in WP options $auth_header = $request->get_header( 'authorization' );

if ( empty( $api_key ) ) { return new WP_Error( 'rest_api_key_not_configured', __( 'API key not configured on server.', 'my-projects' ), array( 'status' => 500 ) ); }

if ( empty( $auth_header ) || ! str_contains( $auth_header, 'Bearer ' ) ) { return new WP_Error( 'rest_unauthorized', __( 'Authorization header is missing or malformed.', 'my-projects' ), array( 'status' => 401 ) ); }

$token = str_replace( 'Bearer ', '', $auth_header );

if ( $token !== $api_key ) { return new WP_Error( 'rest_forbidden', __( 'Invalid API key.', 'my-projects' ), array( 'status' => 403 ) ); }

return true; // Access granted }

With these pieces in place, we've created a custom, secure WordPress REST endpoint at `/wp-json/my-projects/v1/active`. Our internal dashboard can now make a GET request to this URL, providing the correct `Authorization: Bearer YOUR_API_KEY` header, and receive a perfectly formatted JSON array of active projects. This approach empowers developers to precisely control data exposure, optimize payloads, and implement custom authentication flows, transforming WordPress into a truly adaptable backend for any application.