A six-line example in the WordPress documentation provides a working REST API endpoint, but it is not intended for production use. A typical scenario: a developer registers an endpoint that returns 200 KB of JSON for every request, without any authentication or rate limiting. A week later, a bot is flooding it with tens of thousands of requests per hour, the website slows down, and the hosting account’s resource limit is exhausted.
This article is aimed at developers: what to look out for when writing your own endpoint, and how to review existing ones.
The two lines that most examples omit
An endpoint is registered using the register_rest_route() function: namespace, route, HTTP method, the callback that generates the response, and the permission_callback.
permission_callbackdetermines who can call the endpoint. If__return_true, the endpoint is public and anyone can call it from the internet. Since WordPress 5.5, it has been issuing a warning if this is missing.argsdescribes which parameters are accepted. Usingtype,required,sanitize_callbackandvalidate_callback, WordPress rejects invalid input before the code is executed. Without these, anything can reach thecallbackfunction.
Reviewing existing endpoints
WP-CLI lists all non-default endpoints on the website and their access controls (via the cPanel hosting Terminal interface or via SSH):
cd ~/public_html
wp eval 'foreach ( rest_get_server()->get_routes() as $route => $handlers ) { if ( preg_match( "#^/(wp|oembed|wp-site-health|wp-block-editor)/#", $route ) || "/" === $route ) continue; foreach ( $handlers as $h ) { $p = $h["permission_callback"] ?? null; echo $route, " [", implode( ",", array_keys( $h["methods"] ) ), "] ", is_string( $p ) ? $p : ( $p ? "closure" : "NINCS" ), "\n"; } }'
Wherever __return_true appears, the endpoint is public. Check what it returns:
curl -s "https://pelda.hu/wp-json/sajat/v1/adatok" | head -c 1000
If URLs under /wp-json/ return a 404 error, direct links are set to ‘simple’ mode; in this case, use the https://pelda.hu/?rest_route=/sajat/v1/adatok format. If the response displays email addresses, internal IDs or the contents of drafts, the endpoint is leaking data and must be fixed immediately. The basics of WP-CLI are described in the article on using WP-CLI.
Endpoint for live operation
add_action( 'rest_api_init', function () {
register_rest_route( 'sajat/v1', '/termekek/(?P<id>\d+)', array(
'methods' => WP_REST_Server::READABLE, // GET
'callback' => 'sajat_termek_lekerdezese',
'permission_callback' => 'sajat_olvashatja',
'args' => array(
'id' => array(
'required' => true,
'type' => 'integer',
'minimum' => 1,
'sanitize_callback' => 'absint',
),
),
) );
} );
function sajat_olvashatja( WP_REST_Request $request ) {
// Bejelentkezett felhasználó (süti + nonce vagy alkalmazásjelszó).
if ( current_user_can( 'read' ) ) {
return true;
}
// Szerver-szerver kapcsolat API-kulccsal.
$kulcs = $request->get_header( 'X-API-Key' );
if ( $kulcs && defined( 'SAJAT_API_KULCS' ) && hash_equals( SAJAT_API_KULCS, $kulcs ) ) {
return true;
}
return new WP_Error( 'rest_forbidden', 'Hitelesítés szükséges.', array( 'status' => 401 ) );
}
function sajat_termek_lekerdezese( WP_REST_Request $request ) {
$termek = get_post( $request['id'] );
if ( ! $termek || 'publish' !== $termek->post_status ) {
return new WP_Error( 'nincs_ilyen', 'Nincs ilyen termék.', array( 'status' => 404 ) );
}
return rest_ensure_response( array(
'id' => $termek->ID,
'cim' => get_the_title( $termek ),
) );
}
Some rules behind the example:
- Only return what is needed. Not the entire
WP_Post_ object, but just the necessary fields and only the published content. - The API key should not be stored in the database, but in the
wp-config.phpfile (define( 'SAJAT_API_KULCS', '…' );), and you should compare it using thehash_equals()function, not the==operator. - For editorial endpoints (such as data relating to the block editor’s own blocks), permissions should be tied to the relevant capability:
current_user_can( 'edit_post', $id ); never__return_true. sanitize_callbackandvalidate_callback: the first sanitises the input (absint,sanitize_text_field), whilst the second rejects it if it is invalid. For simple type checking, theargsschema (type,minimum,enum,format) is sufficient.
Authentication
- The website’s own JavaScript (for logged-in users) authenticates using the
wp_restnonce: send the value ofwp_create_nonce( 'wp_rest' )in theX-WP-Nonceheader. - External applications, mobile apps and other server applications authenticate using an application password: create this in the user’s profile (Users › Profile, Application Passwords section) and submit it using Basic authentication. The application password can be revoked separately and is not the same as the login password.
Public endpoints: caching and throttling
If the endpoint is intentionally public, protect it from overload.
Caching. WordPress does not cache REST responses. If the content remains valid for several minutes, inform the CDN and the browser:
$valasz = rest_ensure_response( $adatok );
$valasz->header( 'Cache-Control', 'public, max-age=300' );
return $valasz;
You can also store the result of a resource-intensive query on the server as transient data (using set_transient()), so that frequent requests do not have to run it again.
Request limit. WordPress does not limit how many times a user can call an endpoint. The most effective solution is a CDN or firewall rule (such as Cloudflare’s request limit rule for the endpoint’s route), as this prevents excessive requests from even reaching the hosting server. In PHP, a simple counter per IP address at the start of the permission_callback:
function sajat_kereskorlat() {
$kulcs = 'kk_' . md5( $_SERVER['REMOTE_ADDR'] ?? '' );
$db = (int) get_transient( $kulcs );
if ( $db >= 60 ) {
return new WP_Error( 'rest_tul_sok_keres', 'Túl sok kérés.', array( 'status' => 429 ) );
}
set_transient( $kulcs, $db + 1, MINUTE_IN_SECONDS );
return true;
}
This allows 60 requests per minute per IP address. If the website is behind a CDN, REMOTE_ADDR may be the CDN’s address; in this case, take the visitor’s address from the header sent by the CDN (for example, the Cloudflare CF-Connecting-IP header), but only if the request actually originates from the CDN.
Documentation
The register_rest_route() function also accepts a schema parameter, which describes the response in JSON schema. WordPress returns this in response to an OPTIONS request to this endpoint, and it can be processed by OpenAPI exporters and Postman.
Try it out
curl -s -o /dev/null -w "%{http_code}\n" "https://pelda.hu/wp-json/sajat/v1/termekek/123"
curl -s -H "X-API-Key: a-sajat-kulcsa" "https://pelda.hu/wp-json/sajat/v1/termekek/123"
curl -s "https://pelda.hu/wp-json/sajat/v1/termekek/abc"
401 without a key, product details with a key, and 404 or 400 with an invalid identifier are the correct responses. Try this out on a staging copy before going live, and if you need logging to troubleshoot, the article on WordPress debug mode will help. A separate article summarises WordPress’s general security settings.