Blog Add a REST API endpoint to a WordPress plugin
Add a REST API endpoint to a WordPress plugin
TL;DR Use register_rest_route on the rest_api_init hook to add an endpoint. Give it a namespace, a callback that returns a WP_REST_Response, a permission_callback that actually checks access, and an args array that validates input. Never skip the permission check.
The WordPress REST API is one of the most useful things you can build on, and adding your own endpoint is straightforward once you have seen it done cleanly. The trap is doing it carelessly and leaving an open door. Here is the version I ship.
Register the route
Routes get registered on the rest_api_init hook. Give the route a namespace, a path, the HTTP methods it answers, a callback, and a permission callback.
add_action( 'rest_api_init', function () {
register_rest_route(
'myplugin/v1',
'/items/(?P<id>\d+)',
array(
'methods' => WP_REST_Server::READABLE, // GET
'callback' => 'myplugin_get_item',
'permission_callback' => 'myplugin_can_read',
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => function ( $value ) {
return is_numeric( $value );
},
'sanitize_callback' => 'absint',
),
),
)
);
} );
Write the callback
The callback receives the request and returns a response. Wrap real data in rest_ensure_response, and return a WP_Error for problems so the API sends a proper status code.
function myplugin_get_item( WP_REST_Request $request ) {
$id = $request['id']; // already sanitized by absint
$item = get_post( $id );
if ( ! $item || 'my_type' !== $item->post_type ) {
return new WP_Error( 'not_found', 'Item not found', array( 'status' => 404 ) );
}
return rest_ensure_response( array(
'id' => $item->ID,
'title' => get_the_title( $item ),
) );
}
Guard it with a real permission check
This is the part people skip, and it is the part that matters. The permission callback decides access before your handler runs.
function myplugin_can_read() {
// Public data: return true.
// Private data: check a capability.
return current_user_can( 'edit_posts' );
}
If the endpoint exposes anything a random visitor should not see, check a capability. Returning true everywhere is how plugins leak data.
The rules I hold to
- Namespace with a version.
myplugin/v1, never a bare path. - Validate then sanitize every argument. Reject what is malformed, clean what you keep.
- Return WP_Error with a status for failures, so clients get real HTTP codes instead of a 200 with an error buried inside.
- Never trust input, even from a logged-in user. Capabilities and nonces exist for a reason.
Get these habits in from the first endpoint and you will not have to retrofit security later. A custom route is easy. A safe one is a few lines more, and worth every one.
FAQ
Why do I need a permission_callback?
Without it, WordPress warns you and, more importantly, your endpoint may be wide open. The permission_callback runs before your main callback and decides who is allowed in. Return true only for public data, and use a capability check like current_user_can for anything sensitive.
What namespace should I use?
Use your plugin's own namespace with a version, like myplugin/v1. That keeps your routes from colliding with core or other plugins, and the version lets you change the shape later without breaking existing clients.
How do I validate input?
Define each parameter in the args array with a validate_callback and a sanitize_callback. Validation rejects bad input before your handler runs, and sanitization cleans what you keep. Treat every incoming value as untrusted until you have checked it.