2026年11月12日

2026年11月12日

WordPress REST API Not Working? Fix 404, 401, 403 and rest_no_route

This article is a localized edition of the Japanese original. Read the Japanese original

When the WordPress REST API (/wp-json/) does not work, the status code and error code it returns tell you roughly where the problem is. This article first narrows down the symptom, then explains the fixes in this order: the URL does not reach WordPress, the credentials do not reach PHP, code or a plugin blocks the request, and the endpoint is not registered.

First, narrow down the symptom

Replace example.com with your domain. With -i, curl also shows the status code and headers.

# REST API index
curl -i https://example.com/wp-json/

# Form that works regardless of the permalink setting
curl -i "https://example.com/?rest_route=/"

# Fetch one post
curl -i "https://example.com/wp-json/wp/v2/posts?per_page=1"
Result Likely cause Section
/wp-json/ returns 404 (the web server's error page, not WordPress), while ?rest_route=/ works Permalinks are set to "Plain", or the web server has no rewrite configuration The URL does not reach WordPress
JSON with rest_no_route No endpoint is registered for that URL and method The endpoint is not registered
Only authenticated requests get 401 (such as rest_not_logged_in) The Authorization header does not reach PHP, or the credentials are wrong The credentials do not reach PHP
403 with an HTML response instead of JSON A WAF or security feature blocks it Code or a plugin blocks the request
403 or 401 with a JSON error code starting with rest_ Code or a plugin inside WordPress blocks it, or permissions are insufficient Code or a plugin blocks the request

If you can use WP-CLI, you can also list the registered routes.

wp eval 'foreach ( array_keys( rest_get_server()->get_routes() ) as $route ) { echo $route, PHP_EOL; }' | grep '/wp/v2/'

The URL does not reach WordPress (/wp-json/ returns 404)

The /wp-json/ URL is handled by WordPress rewriting (URL rewriting).

  • If permalinks are set to "Plain": /wp-json/ does not work, but the REST API itself works in the ?rest_route=/wp/v2/posts form. WordPress core and the block editor use this form automatically. If you want external programs to call /wp-json/, change the setting under Settings → Permalinks to something like "Post name".
  • If you changed permalinks and still get 404: open Settings → Permalinks and click "Save Changes" without changing anything; this rebuilds the rewrite rules. With WP-CLI, run wp rewrite flush.

If that does not fix it, check the web server configuration.

  • Apache: check that the .htaccess file in the WordPress root has the block from # BEGIN WordPress to # END WordPress, that .htaccess is enabled on the server (the AllowOverride setting), and that mod_rewrite is enabled.
  • Nginx: .htaccess is not used. The site configuration needs the following try_files.
location / {
    try_files $uri $uri/ /index.php?$args;
}

The credentials do not reach PHP (401 with authentication)

Authenticate with an application password

To create or edit posts from an external program through the REST API, application passwords (a built-in feature since WordPress 5.6) are the easiest option. Under Users → Profile, in the "Application Passwords" section, enter a name and generate a new password.

# Check that you can log in (returns your user data on success)
curl -i -u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" \
  https://example.com/wp-json/wp/v2/users/me

# Create one draft
curl -s -u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" \
  -X POST https://example.com/wp-json/wp/v2/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"Test post","status":"draft"}'

admin is the username and xxxx … is the generated application password (not your login password). Application passwords are available only when the site runs over HTTPS (or the environment type is local). If the section does not appear on the profile screen, check that the site uses HTTPS and that no security plugin has disabled the feature.

Apache: the Authorization header does not reach PHP

If you get 401 with the correct password, the web server may not be passing the Authorization header to PHP. This happens most often on Apache running PHP as CGI or FastCGI.

The # BEGIN WordPress block of the .htaccess file that WordPress writes contains the following line for this purpose. Check that it was not removed when the file was edited by hand.

RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

If the header still does not arrive, add one of the following before # BEGIN WordPress.

# Apache 2.4.13 or later (requires AllowOverride AuthConfig)
CGIPassAuth On

# Or, using mod_setenvif
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1

If HTTP_AUTHORIZATION (or REDIRECT_HTTP_AUTHORIZATION) contains a Basic authentication value, WordPress extracts the username and password from it and uses them for authentication.

Nginx: check the fastcgi parameters

By default (fastcgi_pass_request_headers on), Nginx passes request headers to PHP-FPM as parameters starting with HTTP_, so HTTP_AUTHORIZATION normally arrives. If it does not, add the following line to the PHP location to pass it explicitly.

location ~ \.php$ {
    # ... existing fastcgi settings ...
    fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}

If there is another reverse proxy or CDN in front of Nginx, also check that it does not strip the Authorization header. After changing the configuration, check the syntax with sudo nginx -t and apply it with sudo systemctl reload nginx.

Check whether the header arrives

Temporarily place the following file as check-auth.php in the WordPress root and request it with credentials to see what PHP receives. Delete it as soon as you are done.

<?php
// Delete after checking (the values themselves are not shown)
header( 'Content-Type: text/plain' );
echo 'HTTP_AUTHORIZATION: ', isset( $_SERVER['HTTP_AUTHORIZATION'] ) ? 'present' : 'missing', "\n";
echo 'REDIRECT_HTTP_AUTHORIZATION: ', isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ? 'present' : 'missing', "\n";
echo 'PHP_AUTH_USER: ', isset( $_SERVER['PHP_AUTH_USER'] ) ? 'present' : 'missing', "\n";
curl -u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" https://example.com/check-auth.php

If everything is missing, the web server configuration is dropping the header.

Code or a plugin blocks the request (401, 403)

Look for code that disables the REST API

If a theme or plugin has code like the following, which returns an error for every request, the REST API cannot be used. The block editor also uses the REST API, so saving posts stops working too.

add_filter( 'rest_authentication_errors', fn() => new WP_Error( 'rest_disabled', 'REST API disabled' ) );

Search the WordPress files for places that use rest_authentication_errors or rest_enabled (an old filter).

grep -rn "rest_authentication_errors\|rest_enabled" wp-content/themes wp-content/plugins wp-content/mu-plugins

Some security plugins have settings such as "restrict the REST API" or "disable the REST API for logged-out users". Searching the plugin files for the string in the error's code helps you find where the request is blocked. If the response is an HTML 403 page instead of JSON, a WAF in front of WordPress (such as your host's WAF or Cloudflare) is blocking it, so check its logs.

The correct way to restrict access for logged-out users

If you want to restrict the REST API, block only logged-out users instead of everything. The form shown in the FAQ of the developer handbook (REST API Handbook) is as follows.

add_filter( 'rest_authentication_errors', function ( $result ) {
	// If another check already decided the authentication result, pass it through unchanged
	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;
} );

Older articles show an example that blocks requests only when rest_get_url_prefix() does not start with wp-json. Unless changed with a filter, rest_get_url_prefix() always returns wp-json, so the condition never holds and the restriction does nothing.

If you want to allow some endpoints for logged-out users (for example, those used by a contact form plugin), it is easier to decide in the rest_pre_dispatch filter, which receives the request's route.

add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) {
	if ( ! empty( $result ) || is_user_logged_in() ) {
		return $result;
	}
	// Route prefixes allowed without logging in
	$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 );

The value of $allowed is an example. Check the routes of the plugins you actually use in the route list from the WP-CLI command above. str_starts_with() is a PHP 8.0+ function (as of October 2026, the recommended environment for WordPress is PHP 8.3 or later).

The endpoint is not registered (rest_no_route)

Expose a custom post type in the REST API

A custom post type appears in neither the REST API nor the block editor unless show_in_rest is set to true in register_post_type().

add_action( 'init', function () {
	register_post_type( 'product', array(
		'label'        => 'Products',
		'public'       => true,
		'show_in_rest' => true,       // Enable the REST API and the block editor
		'rest_base'    => 'products', // Becomes /wp-json/wp/v2/products
		'supports'     => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
	) );

	// Expose the custom field in the REST API meta (the post type must support custom-fields)
	register_post_meta( 'product', 'price', array(
		'show_in_rest'  => true,
		'single'        => true,
		'type'          => 'number',
		'auth_callback' => function () {
			return current_user_can( 'edit_posts' );
		},
	) );
} );

If a plugin registered the post type, check whether the plugin's settings have an option such as "show in REST API". After registering it, confirm that curl https://example.com/wp-json/wp/v2/products returns a list.

When you create your own endpoint

To create your own endpoint, call register_rest_route() inside the rest_api_init action. permission_callback is required; if you omit it, WordPress issues a notice (_doing_it_wrong).

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 ),
	) );
}

If you get rest_no_route, check that the namespace and route in the URL (myplugin/v1 and /items/123) and the HTTP method (GET or POST) match what you registered. Set permission_callback to '__return_true' only when the endpoint returns public data anyone may read.

Summary

  • Start by trying both /wp-json/ and ?rest_route=/ with curl -i, and narrow down the cause from the status code and error code
  • Even with "Plain" permalinks, the REST API works through ?rest_route=. If /wp-json/ returns 404, re-save the permalinks and check Apache's .htaccess or Nginx's try_files
  • For 401 with authentication, check the application password (HTTPS required) and whether the Authorization header reaches PHP
  • Restrict the REST API by blocking only logged-out users. Examples that check rest_get_url_prefix() do not work
  • For custom post types check show_in_rest; for your own endpoints check register_rest_route() and permission_callback

Related articles:

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲