Custom REST Endpoints
Custom REST endpoints are useful when you need structured data for front-end components, integrations, or automations (without scraping HTML).
Security Baselines
| Concern | What to do |
|---|---|
| Authentication | Require auth for private data |
| Authorization | Check capabilities (current_user_can) |
| Validation | Sanitize parameters and enforce types |
| Output | Return only what the client needs |
caution
Never expose admin-only data or secrets via a public endpoint. Treat REST output like a public API unless you explicitly enforce authentication.
Endpoint Design Guidelines
| Guideline | Why |
|---|---|
Use a namespace + version (gp/v1) | Prevents collisions and enables changes later |
| Keep responses small | Improves performance and reduces leakage |
| Validate and sanitize inputs | Prevents abuse and unexpected queries |
| Return proper errors | Easier debugging for clients |
| Cache when data is expensive | Avoids repeated heavy queries |
Parameter Validation (Example)
Use args to validate query parameters.
Endpoint: validated parameter with sanitize callback
<?php
add_action( 'rest_api_init', function () {
register_rest_route(
'gp/v1',
'/posts',
[
'methods' => 'GET',
'permission_callback' => '__return_true',
'args' => [
'per_page' => [
'type' => 'integer',
'default' => 5,
'sanitize_callback' => 'absint',
],
],
'callback' => function ( WP_REST_Request $req ) {
$per_page = max( 1, min( 20, (int) $req->get_param( 'per_page' ) ) );
$q = new WP_Query(
[
'post_type' => 'post',
'posts_per_page' => $per_page,
'no_found_rows' => true,
]
);
$data = array_map(
static function ( WP_Post $p ) {
return [
'id' => $p->ID,
'title' => get_the_title( $p ),
'url' => get_permalink( $p ),
];
},
$q->posts
);
return rest_ensure_response( $data );
},
]
);
} );
Example: A "Site Info" Endpoint
This endpoint returns safe, public information.
Site plugin: register a simple REST endpoint
<?php
add_action( 'rest_api_init', function () {
register_rest_route(
'gp/v1',
'/site-info',
[
'methods' => 'GET',
'permission_callback' => '__return_true',
'callback' => function () {
return rest_ensure_response(
[
'name' => get_bloginfo( 'name' ),
'url' => home_url( '/' ),
]
);
},
]
);
} );
Test it:
curl: call the endpoint
curl -sL "https://example.com/wp-json/gp/v1/site-info" | jq .
Caching Expensive Responses
If an endpoint performs expensive work (heavy queries, remote calls), cache the response.
Endpoint: cache response with a transient (example)
<?php
add_action( 'rest_api_init', function () {
register_rest_route(
'gp/v1',
'/cached-site-info',
[
'methods' => 'GET',
'permission_callback' => '__return_true',
'callback' => function () {
$key = 'gp_site_info_v1';
$data = get_transient( $key );
if ( false === $data ) {
$data = [
'name' => get_bloginfo( 'name' ),
'url' => home_url( '/' ),
'time' => time(),
];
set_transient( $key, $data, 60 );
}
return rest_ensure_response( $data );
},
]
);
} );
Example: Authenticated Endpoint
If you want private data, enforce permissions.
Endpoint: require a capability
<?php
add_action( 'rest_api_init', function () {
register_rest_route(
'gp/v1',
'/admin-check',
[
'methods' => 'GET',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
'callback' => function () {
return rest_ensure_response( [ 'ok' => true ] );
},
]
);
} );
tip
Put REST endpoints in a site plugin, not in a theme. APIs are site features and should survive theme changes.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 404 from route | Hook not running | Ensure code loads; check rest_api_init |
| 401/403 | Permission callback denies | Verify capability/auth method |
| Wrong data shape | No validation | Sanitize and type-check inputs |
| Endpoint is slow | Heavy query or remote call | Add caching; reduce response size |
Hands-On: Build and Test a Posts Endpoint
- Add the validated
/gp/v1/postsendpoint. - Call it with a few values.
- Confirm it never returns more than 20 items.
Test per_page clamping
curl -sL "https://example.com/wp-json/gp/v1/posts?per_page=3" | jq length
curl -sL "https://example.com/wp-json/gp/v1/posts?per_page=999" | jq length
Quick Reference
- Public endpoint: safe, minimal data only
- Private endpoint: enforce capabilities
- Keep API code in a plugin