REST route with permission callback and schema
Register a REST API route that validates its arguments, authorises with permission_callback and returns a shaped response.
WordPressAPIsrest-apipermission-callbackvalidationjson
Code
<?php
/**
* REST: /wp-json/acme/v1/bookmarks
*
* @package Acme_Core
*/
defined( 'ABSPATH' ) || exit;
add_action( 'rest_api_init', static function (): void {
register_rest_route( 'acme/v1', '/bookmarks', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'acme_rest_list_bookmarks',
'permission_callback' => static fn(): bool => is_user_logged_in(),
'args' => array(
'per_page' => array( 'type' => 'integer', 'default' => 10, 'minimum' => 1, 'maximum' => 50, 'sanitize_callback' => 'absint' ),
),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'acme_rest_add_bookmark',
'permission_callback' => static fn(): bool => current_user_can( 'edit_posts' ),
'args' => array(
'post_id' => array( 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint' ),
'note' => array( 'type' => 'string', 'default' => '', 'maxLength' => 200, 'sanitize_callback' => 'sanitize_text_field' ),
),
),
) );
} );
function acme_rest_list_bookmarks( WP_REST_Request $request ) {
$items = (array) get_user_meta( get_current_user_id(), 'acme_bookmarks', true );
$items = array_slice( array_values( array_filter( $items ) ), 0, (int) $request['per_page'] );
return rest_ensure_response( array( 'items' => $items, 'total' => count( $items ) ) );
}
function acme_rest_add_bookmark( WP_REST_Request $request ) {
$post_id = (int) $request['post_id'];
// Object-level check: the caller may only bookmark posts they can read.
if ( ! get_post( $post_id ) || ! current_user_can( 'read_post', $post_id ) ) {
return new WP_Error( 'acme_not_found', __( 'Post not found.', 'acme-core' ), array( 'status' => 404 ) );
}
$all = (array) get_user_meta( get_current_user_id(), 'acme_bookmarks', true );
$all[ $post_id ] = array( 'post_id' => $post_id, 'note' => (string) $request['note'] );
update_user_meta( get_current_user_id(), 'acme_bookmarks', $all );
return new WP_REST_Response( $all[ $post_id ], 201 );
}
How it works
What it solves
Every REST route that lacks a real permission_callback triggers a notice in current WordPress, and a sloppy callback can expose or change data. This snippet shows a read route for logged-in users and a write route with two layers of authorisation.
How it works
Both handlers are declared on one path, distinguished by HTTP method. Each has an inline permission_callback, so authorisation runs before the handler. Arguments carry a type, bounds, a default and a sanitiser, and WordPress validates and coerces them from the schema before your code sees them. The write handler adds an object-level check with read_post, returns a WP_Error carrying a status for failures, and answers 201 on creation.
No output is built from raw input.
Adapting it
Public data can use __return_true for the permission callback, as long as it is intentional. Move the handlers into a controller class extending WP_REST_Controller when you add item routes with {id}. Add a schema callback to document responses. Rename the namespace, key and capability.
Pitfalls
- Cookie-authenticated requests need the
X-WP-Nonceheader; application passwords work without it. - Capability checks on the route do not replace checks on the specific object.
- Do not echo
$request->get_params()back to the client. - Version the namespace; breaking changes should go to
v2.
How to use it
Save as inc/rest-routes.php and require it from your plugin or theme.
Call GET /wp-json/acme/v1/bookmarks while logged in, sending the REST nonce header.
POST a JSON body with post_id to create a bookmark.
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.