wppaste
WordPress

register_rest_route( string $route_namespace, string $route, array $args = array(), bool $override = false ): bool

Since
4.4.0, 5.1.0, 5.5.0
Source
wp-includes/rest-api.php:34

Adds a custom endpoint to the WordPress REST API under a namespace and route pattern, wiring it to a callback via the global REST server. It only works reliably when called from inside a function hooked to 'rest_api_init'; calling it earlier or on a normal page load triggers a _doing_it_wrong notice and the route will not exist by the time a real REST request comes in. The $args parameter accepts either one method definition or an array of them for endpoints that respond to more than one HTTP verb, and $override decides whether a second registration on the same route replaces or merges with the first.

Registers a REST API route.

Description

Note: Do not use before the 'rest_api_init' hook.

Compatibility

WordPress
since 5.5.0
PHP
7.4–8.6-dev
  • 6.7.7
  • 6.8.8
  • 6.9.7
  • 7.0.4
  • 7.1.0

Present in every tracked release (6.7.7 to 7.1.0), and compiles on PHP 7.4 through 8.6-dev.

Parameters

$route_namespacestring
The first URL segment after core prefix. Should be unique to your package/plugin.
$routestring
The base URL for route you are adding.
$argsarrayoptional
Either an array of options for the endpoint, or an array of arrays for multiple methods. Default empty array.Default: array()
$overridebooloptional
If the route already exists, should we override it? True overrides, false merges (with newer overriding if duplicate keys exist). Default false.Default: false

Return value

bool
True on success, false on error.

Code examples

Every example is editable and runs in a real WordPress booted in your browser by WordPress Playground. Press Run, then edit the code: clicking away re-runs it. Nothing is sent anywhere until you do.

Register a custom REST endpoint that reads a post by ID

Hook a GET route into the REST API that returns a post's title, then dispatch a request to it immediately so the sandbox prints a result.

do_action( 'rest_api_init' );

$registered = register_rest_route(
	'wppaste/v1',
	'/posts/(?P<id>\d+)/title',
	array(
		'methods'             => WP_REST_Server::READABLE,
		'callback'            => function( $request ) {
			$post = get_post( (int) $request['id'] );
			return array( 'title' => $post ? $post->post_title : null );
		},
		'permission_callback' => '__return_true',
	)
);

printf( 'Route registered: %s', $registered ? 'yes' : 'no' );

$request  = new WP_REST_Request( 'GET', '/wppaste/v1/posts/1/title' );
$response = rest_get_server()->dispatch( $request );

echo '<pre>' . esc_html( print_r( $response->get_data(), true ) ) . '</pre>';

In real plugin code the registration call goes inside a function hooked to 'rest_api_init', not after a manual do_action() call; the manual call here only lets the snippet run top to bottom in the sandbox.

Register one route with separate GET and POST handlers using array-of-arrays $args

Expose a single admin-only 'site note' stored in an option, readable by anyone and writable only by users who can manage_options.

do_action( 'rest_api_init' );

register_rest_route(
	'wppaste/v1',
	'/site-note',
	array(
		array(
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => function() {
				return array( 'note' => get_option( 'wppaste_site_note', '' ) );
			},
			'permission_callback' => '__return_true',
		),
		array(
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => function( $request ) {
				update_option( 'wppaste_site_note', sanitize_text_field( $request['note'] ) );
				return array( 'saved' => true );
			},
			'permission_callback' => function() {
				return current_user_can( 'manage_options' );
			},
		),
	)
);

$create = new WP_REST_Request( 'POST', '/wppaste/v1/site-note' );
$create->set_param( 'note', 'Maintenance window Friday.' );
rest_get_server()->dispatch( $create );

$read     = new WP_REST_Request( 'GET', '/wppaste/v1/site-note' );
$response = rest_get_server()->dispatch( $read );

echo esc_html( print_r( $response->get_data(), true ) );

The logged-in administrator in the sandbox satisfies the manage_options permission_callback, so the POST succeeds and the option is updated before the GET reads it back.

Common problems and fixes · 4

Why does my route return a 404 for real visitors even though it worked when I tested it?

The route was registered somewhere that runs on every page load instead of inside a function hooked to 'rest_api_init'. The source explicitly checks did_action( 'rest_api_init' ) and logs a _doing_it_wrong notice when it hasn't fired yet; the REST server that actually answers requests is rebuilt fresh for each REST request, so anything registered outside that hook simply isn't there when a real request arrives.

Why did register_rest_route() return false and nothing got registered?

The function bails out early and returns false whenever $route_namespace or $route is empty, logging a _doing_it_wrong notice for each case rather than throwing a fatal error.

Why does WordPress complain about my namespace even though the route itself works?

The source trims leading and trailing slashes off $route_namespace and compares the result to the original value; if they differ it logs a _doing_it_wrong notice about the namespace containing a slash, even though this check does not stop the route from being registered.

If I call register_rest_route() twice for the same route, which handler wins?

That depends entirely on the $override argument, which defaults to false. With false, the new $args are merged into the existing definition (newer keys win on conflicts); with true, the new $args completely replace whatever was registered before.

Alternatives and related functions

WP_REST_Server::register_route
When you need to register a route without a namespace, such as the main REST index, since register_rest_route() rejects an empty namespace outright.
register_meta
When all you need is to expose an existing post, user, comment, or term meta field through the REST API, rather than build and maintain a custom endpoint.
WP_REST_Controller
When building a reusable endpoint with its own register_routes() method, schema, and permission logic instead of scattering register_rest_route() calls across a plugin.
rest_do_request
When you need to invoke an already-registered REST route from PHP directly, for example to reuse endpoint logic internally, instead of issuing an actual HTTP request to it.

Performance profile

How much work a call to register_rest_route() does, and what it touches: the algorithmic scaling, the Zend instruction count per call across PHP versions, the hooks it hands control to, and the core code that calls it. Measured from the compiled opcodes, not a stopwatch, so every number is identical on any machine running the same PHP version, and every function in core is ranked by cost.

Cost class
Light

Touches nothing outside its own arguments.

Scaling
Scales with input

The body loops, so the work grows with what you pass in.

Instructions
24–75

Executed per call on PHP 8.5, depending on the branch taken. The body compiles to 173.

Plugin surface
None

Nothing here hands control to plugin code.

Called by
43

43 places in core call this, so the cost is paid more often than your own code shows.

What it touches

  • hookthird-party callbacksdo_action()one call below register_rest_route()

Further down the call graph this can also reach query, option, cache, serialize and transient. Those are the worst case, several calls deep and usually down an error path, not what a normal call pays.

What one call costs · 5 distinct outcomes

One number would be a lie: the work depends on which branch runs. These are every distinct cost register_rest_route() can have, taken from its control-flow graph on PHP 8.5.

WhenInstructionsCalls it makes
always24–26__(), sprintf(), _doing_it_wrong()
!empty($route_namespace) && !empty($route) && $clean_namespace === false && did_action()35–40did_action(), rest_get_server(), ->register_route()
!empty($route_namespace) && !empty($route) && $clean_namespace !== false && did_action()52–57__(), sprintf(), _doing_it_wrong(), did_action(), rest_get_server(), ->register_route()
!empty($route_namespace) && !empty($route) && $clean_namespace === false && !did_action()53–58did_action(), __(), sprintf(), _doing_it_wrong(), rest_get_server(), ->register_route()
!empty($route_namespace) && !empty($route) && $clean_namespace !== false && !did_action()70–75__(), sprintf(), _doing_it_wrong(), did_action(), __(), sprintf(), _doing_it_wrong(), rest_get_server(), ->register_route()

Across PHP versions

PHPCompiledExecutedBranchesNotes
8.6-dev17324–7513
8.517324–7513
8.417324–751315 fewer instructions than PHP 8.3
8.318824–8213
8.218824–8213
8.118824–8213
7.418824–8213

An instruction is not a fixed amount of time, so a matching count is not necessarily the same speed; what it rules out is a difference in the work itself.

Uses · 5

  • _doing_it_wrong()Marks something as being incorrectly called.
  • __()Retrieves the translation of $text.
  • did_action()Retrieves the number of times an action has been fired during the current request.
  • rest_get_server()Retrieves the current REST server instance.
  • rest_get_server()::register_route()

Used by · 43

Show all 43

Source code

function register_rest_route( $route_namespace, $route, $args = array(), $override = false ) {	if ( empty( $route_namespace ) ) {		/*		 * Non-namespaced routes are not allowed, with the exception of the main		 * and namespace indexes. If you really need to register a		 * non-namespaced route, call `WP_REST_Server::register_route` directly.		 */		_doing_it_wrong(			__FUNCTION__,			sprintf(				/* translators: 1: string value of the namespace, 2: string value of the route. */				__( 'Routes must be namespaced with plugin or theme name and version. Instead there seems to be an empty namespace \'%1$s\' for route \'%2$s\'.' ),				'<code>' . $route_namespace . '</code>',				'<code>' . $route . '</code>'			),			'4.4.0'		);		return false;	} elseif ( empty( $route ) ) {		_doing_it_wrong(			__FUNCTION__,			sprintf(				/* translators: 1: string value of the namespace, 2: string value of the route. */				__( 'Route must be specified. Instead within the namespace \'%1$s\', there seems to be an empty route \'%2$s\'.' ),				'<code>' . $route_namespace . '</code>',				'<code>' . $route . '</code>'			),			'4.4.0'		);		return false;	} 	$clean_namespace = trim( $route_namespace, '/' ); 	if ( $clean_namespace !== $route_namespace ) {		_doing_it_wrong(			__FUNCTION__,			sprintf(				/* translators: 1: string value of the namespace, 2: string value of the route. */				__( 'Namespace must not start or end with a slash. Instead namespace \'%1$s\' for route \'%2$s\' seems to contain a slash.' ),				'<code>' . $route_namespace . '</code>',				'<code>' . $route . '</code>'			),			'5.4.2'		);	} 	if ( ! did_action( 'rest_api_init' ) ) {		_doing_it_wrong(			__FUNCTION__,			sprintf(				/* translators: 1: rest_api_init, 2: string value of the route, 3: string value of the namespace. */				__( 'REST API routes must be registered on the %1$s action. Instead route \'%2$s\' with namespace \'%3$s\' was not registered on this action.' ),				'<code>rest_api_init</code>',				'<code>' . $route . '</code>',				'<code>' . $route_namespace . '</code>'			),			'5.1.0'		);	} 	if ( isset( $args['args'] ) ) {		$common_args = $args['args'];		unset( $args['args'] );	} else {		$common_args = array();	} 	if ( isset( $args['callback'] ) ) {		// Upgrade a single set to multiple.		$args = array( $args );	} 	$defaults = array(		'methods'  => 'GET',		'callback' => null,		'args'     => array(),	); 	foreach ( $args as $key => &$arg_group ) {

Changelog

Introduced in 4.4.0. Unchanged from 6.7.7 through 7.1.0.

  1. 6.7.7
  2. 6.8.8
  3. 6.9.7
  4. 7.0.4
  5. 7.1.0

Signature, return type and hooks compared across 5 parsed releases.

5.5.0
Added a _doing_it_wrong() notice when the required permission_callback argument is not set.from the docblock
5.1.0
Added a _doing_it_wrong() notice when not called on or after the rest_api_init hook.from the docblock
4.4.0
Introduced.from the docblock

About this page

Parsed data
Generated from the wordpress-develop 7.1.0 tag, from src/wp-includes/rest-api.php, and regenerated for each WordPress release so it tracks the code rather than a snapshot of it.
Corrections
Something wrong on this page? Report it and it gets fixed in the next regeneration.