As WordPress developers, we frequently encounter scenarios where the default REST API endpoints just don’t cut it. Perhaps we need to expose highly specific data structures from a custom plugin, filter content in a unique way, or integrate with an external dashboard that demands a very particular API contract. Rolling your own custom REST endpoint provides the flexibility to meet these exact demands, allowing you to tailor data output and, crucially, secure access to it. Today, I want to walk through building such an endpoint, focusing on a concrete example: exposing “active” projects from a custom plugin, authenticated via WordPress’s native Application Passwords.
Imagine we’ve built a custom plugin that manages “Projects” as a Custom Post Type (CPT), complete with custom fields like project_status. We need an API endpoint to list all currently “active” projects, optionally allowing a search query, for consumption by an external analytics dashboard. This endpoint must only be accessible to authorized applications, not just anyone on the internet.
Our first step is to register our custom route within the WordPress REST API. We do this by hooking into rest_api_init. Here, we define the route’s namespace, path, HTTP method, a callback function to handle the data, and most importantly, a permission callback to enforce access control. I’ll also add a simple argument for search.
add_action( 'rest_api_init', 'my_plugin_register_project_endpoint' );
function my_plugin_register_project_endpoint() {
register_rest_route( 'my-plugin/v1', '/projects/active', array(
'methods' => 'GET',
'callback' => 'my_plugin_get_active_projects',
'permission_callback' => 'my_plugin_check_project_permissions',
'args' => array(
'search' => array(
'description' => __( 'Search string for project titles.', 'my-plugin' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
),
) );
}
Next, let’s implement the my_plugin_get_active_projects callback function. This function will receive a WP_REST_Request object, allowing us to access any query parameters (like our ‘search’ term). Inside, we’ll use a standard WP_Query to fetch our ‘project’ CPTs, filtered by a custom field project_status set to ‘active’, and format the results for JSON output.
function my_plugin_get_active_projects( WP_REST_Request $request ) {
$args = array(
'post_type' => 'project', // Assuming 'project' is a custom post type
'post_status' => 'publish',
'meta_query' => array(
array(
'key' => 'project_status', // Assuming a custom field for project status
'value' => 'active',
'compare' => '=',
),
),
'posts_per_page' => -1,
);
$search = $request->get_param( 'search' );
if ( ! empty( $search ) ) {
$args['s'] = $search;
}
$projects_query = new WP_Query( $args );
$projects = array();
if ( $projects_query->have_posts() ) {
while ( $projects_query->have_posts() ) {
$projects_query->the_post();
$projects[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'description' => get_the_excerpt(),
'url' => get_permalink(),
'status' => get_post_meta( get_the_ID(), 'project_status', true ),
);
}
wp_reset_postdata();
}
return new WP_REST_Response( $projects, 200 );
}
Now for the crucial part: authentication. The permission_callback function determines whether the current request is authorized to access the endpoint. WordPress’s native Application Passwords offer a robust, secure way for external applications to authenticate. When an external app sends a request with an Application Password in the Authorization header (Basic Auth format), WordPress authenticates the request against the user who owns that password. Our permission_callback then simply checks if that authenticated user has the necessary capabilities. For our example, we’ll check for edit_posts, but for real-world scenarios, a custom capability like read_active_projects is always preferred for granular control.
function my_plugin_check_project_permissions( WP_REST_Request $request ) {
// WordPress handles the Application Password validation;
// we just need to check the capabilities of the authenticated user.
return current_user_can( 'edit_posts' );
}
To test this, you would generate an Application Password for a user (e.g., an administrator or editor) from their profile page in the WordPress dashboard. Then, your external application would make a GET request to /wp-json/my-plugin/v1/projects/active with a Basic Authorization header, using the username and Application Password as credentials. For example, Authorization: Basic [base64_encode(username:application_password)].
By following this pattern, you gain complete control over how your WordPress data is exposed and consumed, moving beyond the standard API to create custom, secure, and performant endpoints perfectly tailored to your infrastructure needs. This capability is fundamental for building sophisticated WordPress-powered ecosystems.