Exposing Secure Post Meta via Custom WordPress REST API Endpoints
Learn how to create a secure custom WordPress REST API endpoint to expose specific post meta data only to authorized users, preventing sensitive data leaks.
29 Dec 2025, 02:49 UTC

The Challenge of Secure Meta Exposure
By default, WordPress hides most post meta (custom fields) from the REST API to prevent the accidental leak of sensitive data. While you can set 'show_in_rest' => true when registering meta, this makes the data public to anyone who can view the post. When you need to expose specific meta only to authorized users—such as internal SKU numbers, private client notes, or administrative flags—you must build a custom endpoint with a dedicated permission check.
Implementing a Protected Meta Endpoint
To create a secure endpoint, you use register_rest_route(). This function requires a namespace (to avoid collisions with other plugins), a route pattern, and a set of arguments that define how the request is handled and who is allowed to access it.
Add the following code to a custom plugin or your theme's functions.php file. This example assumes you are using WordPress 5.0+.
add_action('rest_api_init', function () {
register_rest_route('myplugin/v1', '/post-meta/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => 'get_secure_post_meta_handler',
'permission_callback' => 'verify_meta_access_permissions',
]);
});
/**
* Permission check: Ensures the user has rights to the specific post.
*/
function verify_meta_access_permissions($request) {
$post_id = $request['id'];
// Only allow users who can edit this specific post to see its meta
return current_user_can('edit_post', $post_id);
}
/**
* Data handler: Retrieves and returns the meta data.
*/
function get_secure_post_meta_handler($request) {
$post_id = intval($request['id']);
// Define which meta keys are safe to expose
$allowed_keys = ['_internal_reference_id', '_private_client_note'];
$results = [];
foreach ($allowed_keys as $key) {
$value = get_post_meta($post_id, $key, true);
if ($value) {
$results[$key] = $value;
}
}
if (empty($results)) {
return new WP_Error('no_meta', 'No authorized meta found for this post', ['status' => 404]);
}
return new WP_REST_Response($results, 200);
}
Mechanism Breakdown
- The Route Pattern:
(?P<id>\d+)is a regular expression that captures digits from the URL and assigns them to a variable namedidin the request object. - The Permission Callback: This is the most critical security layer. By using
current_user_can('edit_post', $post_id), the API automatically returns a 401 (Unauthorized) or 403 (Forbidden) response if the requester is not logged in or lacks sufficient privileges. - The Allowed Keys List: Instead of returning all meta via
get_post_custom(), we use an explicit whitelist. This prevents the accidental exposure of WordPress internal keys (those starting with underscores) that you didn't intend to share.
Common Implementation Mistakes
| Mistake | Risk | Correction |
|---|---|---|
Omitting permission_callback |
Data Leakage | Always define a callback; never return true globally. |
Using $_GET['id'] directly |
Injection/Errors | Use $request['id'] and wrap in intval(). |
| Returning all meta data | Information Disclosure | Use a whitelist of specific meta keys. |
Verification and Testing
To verify the endpoint is functioning and secure, perform these three checks using a tool like cURL or Postman:
- Unauthorized Access: Request
/wp-json/myplugin/v1/post-meta/123without an authentication cookie or token. You should receive arest_forbiddenerror. - Authorized Access: Log in as an Administrator or the post author and request the same URL. You should receive a JSON object containing only the whitelisted keys.
- Invalid ID: Request a non-existent ID (e.g.,
/post-meta/999999). The system should return a 404 or theno_metaerror defined in the callback.
Limitations
This approach is designed for server-to-server requests or authenticated frontend requests. If you are calling this from a decoupled frontend (like React or Vue), you must handle authentication via Application Passwords or JWT (JSON Web Tokens), as the REST API does not accept standard session cookies across different domains (CORS).
Rollback
To disable the endpoint, remove the add_action('rest_api_init', ...) block from your code. Since this operation only registers a route in memory during the request lifecycle and does not modify the database, no data cleanup is required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.