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.