The Problem
Every WordPress install answers REST requests at /wp-json/ whether or not anyone deliberately built an API — and most sites have at least one custom field, integration, or automation hooked into it without a clear picture of which of these five hooks actually runs first, or what happens if two of them disagree about whether a request should even be allowed through. The most common version of this problem: a developer adds an authentication check in the wrong hook, and it either never runs early enough to block anything, or it overwrites an error another check already set — silently reopening a door someone else had just closed.
Why This Is Dangerous
These hooks don’t fire in registration order — they fire in a fixed pipeline order regardless of when your code adds them, and getting that order wrong changes what your check can actually see or prevent. A permission check added to the wrong hook runs after the request has already been routed and partially processed, which means “blocking” it there is really just hiding the result, not preventing the work that already happened. And rest_authentication_errors specifically is a filter that receives whatever error a previous check already set — overwriting that value unconditionally silently discards someone else’s rejection and can open access nobody intended to leave open. For deeper coverage of designing custom endpoints and their permission model, see Custom Endpoints & Routes and Authentication & Permissions on 4wp.dev.
Step-by-Step Solution — The Pipeline, in Order
Step 1 — `rest_api_init` (action) — where every custom route gets registered
add_action( 'rest_api_init', function() {
register_rest_route( '4wp/v1', '/widgets', array(
'methods' => 'GET',
'callback' => 'forwp_get_widgets',
'permission_callback' => '__return_true',
) );
} );
This fires once, early, before any specific request is being handled — it’s registration, not request handling. Nothing here should depend on the current request’s data; that comes later in the pipeline.
Step 2 — `rest_pre_dispatch` (filter) — the earliest point to short-circuit a request entirely
add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) {
if ( str_starts_with( $request->get_route(), '/4wp/v1/' ) && forwp_is_maintenance_mode() ) {
return new WP_Error( 'forwp_maintenance', 'API temporarily unavailable.', array( 'status' => 503 ) );
}
return $result;
}, 10, 3 );
Returning a non-null value here skips routing and the endpoint’s own callback entirely — the right tool for a blanket condition (maintenance mode, a global rate limit) that should apply before any specific route’s own logic runs, not a substitute for per-route permission_callback checks.
Step 3 — `rest_authentication_errors` (filter) — respect what’s already there, or you’ll undo someone else’s rejection
add_filter( 'rest_authentication_errors', function( $result ) {
// If a previous check already set an error, don't overwrite it —
// or you silently discard someone else's rejection.
if ( null !== $result ) {
return $result;
}
if ( forwp_request_is_from_blocked_ip() ) {
return new WP_Error( 'forwp_blocked', 'Access denied.', array( 'status' => 403 ) );
}
return $result;
} );
$result is null when authentication hasn’t yet failed or succeeded, true/a user object on success, or a WP_Error on failure — the same three-state contract that trips people up on the authenticate hook in the Auth category. Skipping the null !== $result check here is exactly the mistake that overwrites another plugin’s authentication rejection with your own permissive default.
Step 4 — `rest_prepare_post` (filter) — shaping the response before it leaves the server
add_filter( 'rest_prepare_post', function( $response, $post, $request ) {
$data = $response->get_data();
unset( $data['internal_notes'] ); // Never let an internal-only field reach the public response.
$response->set_data( $data );
return $response;
}, 10, 3 );
This is the last checkpoint before a post’s REST representation is sent — the correct place both to add computed fields and to strip anything that shouldn’t be public, rather than trying to prevent a field from being queryable in the first place.
Step 5 — `rest_insert_post` (action) — side effects after a REST-driven write
add_action( 'rest_insert_post', function( $post, $request, $creating ) {
if ( $creating ) {
forwp_notify_new_post_via_api( $post->ID );
}
}, 10, 3 );
Fires after a post is created or updated specifically through the REST API — the $creating boolean tells you which. Logic that should run regardless of how a post was saved (API or editor) belongs on wp_insert_post instead, covered in the Save & CRUD Hooks category.
When It’s Better to Bring In a Specialist
Getting the order of these five hooks wrong doesn’t usually throw a visible error — it just means a permission check runs too late to matter, or a public response leaks a field nobody meant to expose. A WordPress developer who has built and audited custom REST APIs checks the whole pipeline order as one system, the same discipline covered in more depth in REST API Development on 4wp.dev — outsourcing that specific review to someone who has already made and fixed these mistakes elsewhere is usually faster than tracing a silent permission gap through five hooks yourself.
FAQ
rest_pre_dispatch can short-circuit a request entirely, before routing, for conditions that apply broadly (maintenance mode, global rate limits). rest_authentication_errors is specifically about identity and access — and it can undo another check’s rejection if you don’t respect an existing non-null $result.
No — they fire in a fixed pipeline order determined by the REST API’s own request lifecycle (rest_pre_dispatch → authentication → routing → rest_prepare_* → rest_insert_*), regardless of when your plugin registers its callbacks.
Almost certainly because the filter didn’t check whether $result was already non-null before returning its own value — overwriting an existing WP_Error (or a success) with your own default silently replaces the other check’s decision.
register_rest_field for a straightforward computed value tied to the schema. rest_prepare_post when you need to both add and remove fields, or when the logic depends on the full response object rather than a single field’s value.
No — it fires specifically for posts created or updated through a REST API request. A normal editor save fires wp_insert_post and save_post, not rest_insert_post.
When multiple plugins or custom code hook into authentication and response-shaping on the same endpoints — at that point, tracing which check actually wins requires understanding the full pipeline order, which is exactly the kind of audit worth outsourcing rather than reconstructing from scratch under time pressure.
Summary
A full breakdown of each of the 5 hooks in this category — with examples and a hands-on IDE — is on the REST API Hooks page, and the whole set is also available as one PDF to keep. For the broader discipline of building custom REST APIs on WordPress, see REST API Development on 4wp.dev.


