2026年11月12日
2026年11月12日
WordPress REST API Not Working: Code-Side Causes and Correct Patterns
This article is a localized edition of the Japanese original. Read the Japanese original
The causes of a WordPress REST API (/wp-json/) that does not work fall into two groups: "server settings (permalinks, rewrites, the Authorization header)" and "theme or plugin code". This article focuses on the second group, the code side, and explains common mistakes and the correct way to write them. Server-side causes (such as /wp-json/ returning 404, or 401 even with an application password) are covered in detail in How to fix the WordPress REST API when it does not work.
The code examples work with WordPress 7.1 and PHP 8.3 as of October 2026.
Check the symptoms first
Replace example.com with your own domain.
# Is the REST API working at all? (-i also shows the status code)
curl -i https://example.com/wp-json/
# A form that works regardless of the permalink setting
curl -i "https://example.com/?rest_route=/wp/v2/posts&per_page=1"
| Symptom | Usual cause | Section to read |
|---|---|---|
Every endpoint returns 401 or 403, and the error code starts with rest_ |
Theme or plugin code is blocking the REST API | Fix code that blocks the REST API |
Only the URL for a specific post type returns rest_no_route |
show_in_rest is not enabled |
Expose custom post types, taxonomies and meta |
| Calls from JavaScript in the admin or front end return 401 or 403 even while logged in | The nonce (X-WP-Nonce) is not sent, or it is stale |
Nonces when calling from JavaScript |
A custom endpoint returns rest_no_route |
Registered at the wrong time, or the namespace or method does not match | Register custom endpoints correctly |
If WP-CLI is available, you can list the registered routes.
wp eval 'foreach ( array_keys( rest_get_server()->get_routes() ) as $route ) { echo $route, PHP_EOL; }'
Fix code that blocks the REST API
An example that blocks everything
If the rest_authentication_errors filter always returns an error, as below, none of the REST API can be used. The block editor also uses the REST API, so posts can no longer be saved.
add_filter( 'rest_authentication_errors', fn() => new WP_Error( 'rest_disabled', 'REST API disabled' ) );
Search your themes and plugins for code like this.
grep -rn "rest_authentication_errors" wp-content/themes wp-content/plugins wp-content/mu-plugins
An example of a restriction that does nothing
Some examples intended to block only logged-out users are written like this.
// Wrong: this condition is never true, so nothing is restricted
add_filter( 'rest_authentication_errors', function ( $result ) {
if ( ! is_user_logged_in() && ! str_starts_with( rest_get_url_prefix(), 'wp-json' ) ) {
return new WP_Error( 'rest_not_logged_in', 'Authentication required', array( 'status' => 401 ) );
}
return $result;
} );
rest_get_url_prefix() does not return the requested URL; it returns the REST API URL prefix (wp-json by default). Unless you changed it with the rest_url_prefix filter, it is always wp-json, so ! str_starts_with( ... ) is always false and this restriction does nothing.
Blocking only logged-out users
This is the pattern shown in the FAQ of the REST API Handbook for developers. If other code has already decided the authentication result, it returns that result unchanged.
<?php
add_filter( 'rest_authentication_errors', function ( $result ) {
if ( true === $result || is_wp_error( $result ) ) {
return $result;
}
if ( ! is_user_logged_in() ) {
return new WP_Error( 'rest_not_logged_in', 'You must be logged in.', array( 'status' => 401 ) );
}
return $result;
} );
This pattern also blocks features where logged-out visitors use the REST API, such as contact form plugins. To allow only some routes, decide in the rest_pre_dispatch filter, which receives the route.
<?php
add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) {
if ( ! empty( $result ) || is_user_logged_in() ) {
return $result;
}
// Route prefixes that logged-out visitors may use (example)
$allowed = array( '/contact-form-7/v1/' );
foreach ( $allowed as $prefix ) {
if ( str_starts_with( $request->get_route(), $prefix ) ) {
return $result;
}
}
return new WP_Error( 'rest_not_logged_in', 'You must be logged in.', array( 'status' => 401 ) );
}, 10, 3 );
Put the routes you actually use in $allowed, after checking them with the WP-CLI command above.
Expose custom post types, taxonomies and meta
Custom post types
Unless show_in_rest is set to true in register_post_type(), the post type appears neither in the REST API nor in the block editor.
<?php
add_action( 'init', function () {
register_post_type( 'product', array(
'label' => 'Products',
'public' => true,
'show_in_rest' => true, // Support the REST API and the block editor
'rest_base' => 'products', // Becomes /wp-json/wp/v2/products
'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
) );
// Custom taxonomies also need show_in_rest (otherwise they do not appear in the editor)
register_taxonomy( 'product_cat', 'product', array(
'label' => 'Product categories',
'hierarchical' => true,
'show_in_rest' => true,
) );
} );
After registering, check that curl https://example.com/wp-json/wp/v2/products returns a list. For a post type registered by a plugin, look for a REST API option in that plugin's settings.
Custom fields (meta)
Only post meta registered with show_in_rest through register_post_meta() appears in the meta of the response. The post type's supports must include custom-fields.
<?php
add_action( 'init', function () {
register_post_meta( 'product', 'price', array(
'show_in_rest' => true,
'single' => true,
'type' => 'number',
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
) );
} );
auth_callback decides whether this meta may be changed, for example through the REST API. For reading only, anyone can see the meta of published posts.
Adding your own fields to the response
To add values other than meta (such as calculated values) to the response, use register_rest_field() rather than rewriting data directly in the rest_prepare_post filter. The field appears in the schema, so it shows up in the endpoint description (OPTIONS requests), and when fields are narrowed with the _fields parameter, its value is calculated only when that field is requested.
<?php
add_action( 'rest_api_init', function () {
register_rest_field( 'post', 'reading_minutes', array(
'get_callback' => function ( $post ) {
$chars = mb_strlen( wp_strip_all_tags( get_post_field( 'post_content', $post['id'] ) ) );
return max( 1, (int) ceil( $chars / 600 ) ); // Assumes 600 characters per minute
},
'schema' => array(
'description' => 'Estimated reading time (minutes)',
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
) );
} );
Nonces when calling from JavaScript
When a logged-in user's browser calls the REST API with fetch() or similar (cookie authentication), WordPress checks whether the request carries a nonce created with the action name wp_rest.
- Without a nonce, the request is treated as coming from a logged-out user even while logged in, and operations that need permissions return 401 or similar
- With a stale or wrong nonce, you get
rest_cookie_invalid_nonce(403)
Pass the nonce and URL to your script from PHP, and send it in the X-WP-Nonce header. my-script is an example script handle.
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script( 'my-script', get_theme_file_uri( 'js/my-script.js' ), array(), '1.0', true );
wp_add_inline_script(
'my-script',
'const myApi = ' . wp_json_encode( array(
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) ) . ';',
'before'
);
} );
// js/my-script.js
fetch( myApi.root + 'wp/v2/users/me', {
headers: { 'X-WP-Nonce': myApi.nonce },
credentials: 'same-origin',
} )
.then( ( res ) => res.json() )
.then( ( user ) => console.log( user.name ) );
If your block editor script uses @wordpress/api-fetch (wp.apiFetch), the nonce and URL are set automatically, so you do not need this.
If pages are cached, the nonce embedded in the HTML can go stale and cause rest_cookie_invalid_nonce. Check that cached pages are not served to logged-in users.
When calling from an external program or server, use an application password (created under Users → Profile) instead of cookie authentication.
Register custom endpoints correctly
Register custom endpoints by calling register_rest_route() inside the rest_api_init action. Calling it at another time, such as init, does not register it correctly. permission_callback cannot be omitted; if you omit it, WordPress issues a notice (_doing_it_wrong).
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/items/(?P<id>\d+)', array(
'methods' => WP_REST_Server::READABLE, // GET
'callback' => 'myplugin_rest_get_item',
'permission_callback' => function () {
return current_user_can( 'read' );
},
'args' => array(
'id' => array(
'validate_callback' => function ( $value ) {
return is_numeric( $value );
},
'sanitize_callback' => 'absint',
'required' => true,
),
),
) );
} );
function myplugin_rest_get_item( WP_REST_Request $request ) {
$post = get_post( $request->get_param( 'id' ) );
if ( ! $post instanceof WP_Post ) {
return new WP_Error( 'myplugin_not_found', 'Not found.', array( 'status' => 404 ) );
}
return rest_ensure_response( array(
'id' => $post->ID,
'title' => get_the_title( $post ),
) );
}
The URL for this example is https://example.com/wp-json/myplugin/v1/items/123. If you get rest_no_route, check that the namespace (myplugin/v1), the route (/items/123) and the HTTP method (GET or POST) match what you registered. Set permission_callback to '__return_true' only when returning public data that anyone may read.
Summary
- A restriction based on
rest_get_url_prefix()does nothing. To block only logged-out users, writerest_authentication_errorsso it respects a result that has already been decided. To allow only some routes, userest_pre_dispatch - Custom post types and taxonomies need
show_in_rest; meta needsshow_in_restinregister_post_meta()(andcustom-fieldssupport) - Add fields to the response with
register_rest_field() - From the browser, send a
wp_restnonce inX-WP-Nonce. From outside, use an application password - Register custom endpoints inside
rest_api_initand specifypermission_callback
Related articles:
- How to fix the WordPress REST API when it does not work: causes of 404, 401, 403 and rest_no_route
- Causes and fixes when the WordPress REST API returns "400 Bad Request"
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)