When WordPress acts as a headless backend or a service API, extending its default REST capabilities becomes essential. We often need to expose custom data types with highly specific access rules. My focus today is demonstrating how to design and implement a secure custom REST endpoint for user-owned data, ensuring users can only retrieve their own content while administrators retain full access.
Consider a system where users manage “Projects,” each a custom post type linked to its author. The core requirement: User A sees only their projects, never User B‘s. First, a minimal Custom Post Type definition for project. While show_in_rest can expose it via the default API, we’re building a custom route for granular control. The author support is crucial for tracking ownership.
function my_plugin_register_project_cpt() {
$labels = ['name' => 'Projects', 'singular_name' => 'Project'];
$args = [
'labels' => $labels, 'public' => true,
'has_archive' => true, 'rewrite' => ['slug' => 'projects'],
'show_in_rest' => true,
'supports' => ['title', 'editor', 'author'],
'capability_type' => 'post', 'map_meta_cap' => true,
];
register_post_type('project', $args);
}
add_action('init', 'my_plugin_register_project_cpt');
The core of our solution is register_rest_route, hooked into rest_api_init. Here, we define our endpoint (/my-plugin/v1/projects), its allowed methods (GET via WP_REST_Server::READABLE), the data retrieval logic (callback), and critically, the initial security gate (permission_callback).
function my_plugin_register_project_rest_route() {
register_rest_route('my-plugin/v1', '/projects', [
'methods' => WP_REST_Server::READABLE,
'callback' => 'my_plugin_get_user_projects',
'permission_callback' => 'my_plugin_can_access_projects',
'args' => [], // Define validation for query parameters here if needed
]);
}
add_action('rest_api_init', 'my_plugin_register_project_rest_route');
The my_plugin_can_access_projects function serves as our security pre-check. Its only job is to authorize access to the route itself, not to filter specific data. It verifies the requesting user is logged in and possesses a baseline capability (read). If these fail, we return a WP_Error with an appropriate HTTP status, preventing the request from proceeding to data retrieval.
function my_plugin_can_access_projects(WP_REST_Request $request) {
if (!is_user_logged_in()) {
return new WP_Error('rest_forbidden', 'You are not logged in.', ['status' => 401]);
}
if (!current_user_can('read')) {
return new WP_Error('rest_forbidden_capability', 'You lack required permissions.', ['status' => 403]);
}
return true; // Authentication and basic capability checks passed.
}
This permission callback handles general route access. The actual data scoping—returning only a user’s projects versus all projects—occurs within my_plugin_get_user_projects. This separation keeps our security gate focused and our data callback responsible for business logic: querying and filtering based on the authenticated user’s role and ID.
function my_plugin_get_user_projects(WP_REST_Request $request) {
$args = ['post_type' => 'project', 'posts_per_page' => -1, 'post_status' => 'publish'];
$current_user_id = get_current_user_id();
// Restrict query to current user's projects unless they are an administrator.
if (!user_can($current_user_id, 'manage_options')) {
$args['author'] = $current_user_id;
}
$projects_query = new WP_Query($args);
$projects_data = [];
if ($projects_query->have_posts()) {
while ($projects_query->have_posts()) {
$projects_query->the_post();
$projects_data[] = [
'id' => get_the_ID(),
'title' => get_the_title(),
'content' => apply_filters('the_content', get_the_content()),
'date' => get_the_date('c'),
'author_id' => get_the_author_meta('ID'),
];
}
wp_reset_postdata();
}
return new WP_REST_Response($projects_data, 200);
}
For authentication, logged-in browser users benefit from automatic cookie transmission, enabling is_user_logged_in() and get_current_user_id(). For external applications, leverage Application Passwords or OAuth for secure, token-based authentication against these custom endpoints. This pattern empowers you to build secure, decoupled, and highly custom experiences with WordPress as a robust backend.