The WordPress REST API is powerful out of the box, but real projects often need data shaped exactly for a specific app or integration. Registering your own endpoints lets you expose precisely what you want, with the permissions and validation you choose.
The Building Block: register_rest_route
Every custom endpoint starts with register_rest_route(), called on the rest_api_init hook. You give it a namespace, a route, and an array describing the HTTP method, the callback, and a permission check.
add_action( 'rest_api_init', function () {
register_rest_route( 'myapp/v1', '/stats', array(
'methods' => 'GET',
'callback' => 'myapp_get_stats',
'permission_callback' => 'myapp_check_permission',
) );
} );
Namespacing Matters
Always namespace your routes, like myapp/v1. The version segment lets you ship a v2 later without breaking existing clients, and the prefix avoids collisions with other plugins.
Writing the Callback
Your callback receives a request object and should return data or a WP_REST_Response. Keep the logic focused: gather your data, shape it into a clean array, and return it. WordPress handles the JSON serialization.
Reading Parameters Safely
Pull parameters with $request->get_param( 'id' ) and always sanitize them. Define an args array in your route to declare validation and sanitization callbacks so bad input never reaches your logic.
Never Skip the Permission Callback
The permission_callback is not optional. Returning true makes your endpoint fully public, which is rarely what you want for write operations. Use current_user_can() to gate access by capability.
function myapp_check_permission() {
return current_user_can( 'edit_posts' );
}
Handling Errors Gracefully
Return a WP_Error with a proper status code when something goes wrong. The REST API converts it into a clean JSON error response with the right HTTP status, which clients can handle predictably.
Testing Your Endpoint
Hit your route at /wp-json/myapp/v1/stats in the browser or with curl. For authenticated routes, use an Application Password so you can test permission logic without a full login flow.
Validate and Sanitize Arguments
Declaring an args array on your route lets you attach a validate_callback and a sanitize_callback to each parameter. Validation rejects malformed input before your code runs, and sanitization cleans what does get through. Together they keep your endpoint robust against both honest mistakes and deliberate abuse.
Shaping Clean Responses
Resist the urge to dump raw database rows into your response. Map your data to a deliberate structure with clear field names, and avoid leaking internal IDs or sensitive fields. A well-shaped response is easier for client developers to consume and safer to expose publicly.
Versioning for the Long Run
Because your namespace includes a version, you can introduce breaking changes in a new version while leaving the old one untouched. Existing integrations keep working against v1 while new clients adopt v2. This discipline saves you from the painful choice between breaking users and never improving your API.
Document Your Endpoints
An API is only as useful as it is understandable. Keep a short reference for each route that lists the URL, accepted parameters, required permissions, and an example response. Whether the consumer is a teammate or your future self, clear documentation turns a custom endpoint from a mystery into a dependable building block. It also surfaces inconsistencies in your design before they become support headaches in production.
Handle Authentication for External Clients
When a mobile app or external service needs to call your protected endpoints, it cannot log in through the browser the way a person does. Application Passwords solve this cleanly: generate one for the connecting service and send it with each request using standard authentication headers. Your permission callbacks then evaluate that user’s capabilities exactly as they would for a normal login. This keeps your endpoints secure while still being callable by automation, and because each Application Password can be revoked individually, you can cut off a single integration without disrupting everything else that talks to your site.
The Takeaway
Namespace your routes, sanitize every input, always enforce permissions, and return structured errors. Do that and your custom endpoints will be secure, versioned, and a joy to consume.
