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.
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?
- Why did register_rest_route() return false and nothing got registered?
- Why does WordPress complain about my namespace even though the route itself works?
- If I call register_rest_route() twice for the same route, which handler wins?
Why does my route return a 404 for real visitors even though it worked when I tested it?
Why did register_rest_route() return false and nothing got registered?
Why does WordPress complain about my namespace even though the route itself works?
If I call register_rest_route() twice for the same route, which handler wins?
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
- Scaling
- Scales with input
- Instructions
- 24–75
- Plugin surface
- None
- Called by
- 43
Touches nothing outside its own arguments.
The body loops, so the work grows with what you pass in.
Executed per call on PHP 8.5, depending on the branch taken. The body compiles to 173.
Nothing here hands control to plugin code.
43 places in core call this, so the cost is paid more often than your own code shows.
What it touches
- hookthird-party callbacks
do_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.
| When | Instructions | Calls it makes |
|---|---|---|
| always | 24–26 | __(), sprintf(), _doing_it_wrong() |
!empty($route_namespace) && !empty($route) && $clean_namespace === false && did_action() | 35–40 | did_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–58 | did_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
| PHP | Compiled | Executed | Branches | Notes |
|---|---|---|---|---|
| 8.6-dev | 173 | 24–75 | 13 | |
| 8.5 | 173 | 24–75 | 13 | |
| 8.4 | 173 | 24–75 | 13 | 15 fewer instructions than PHP 8.3 |
| 8.3 | 188 | 24–82 | 13 | |
| 8.2 | 188 | 24–82 | 13 | |
| 8.1 | 188 | 24–82 | 13 | |
| 7.4 | 188 | 24–82 | 13 |
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
- WP_REST_Abilities_V1_Categories_Controller::register_routes()Registers the routes for ability categories.
- WP_REST_Abilities_V1_List_Controller::register_routes()Registers the routes for abilities.
- WP_REST_Abilities_V1_Run_Controller::register_routes()Registers the routes for ability execution.
- WP_REST_Application_Passwords_Controller::register_routes()Registers the REST API routes for the application passwords controller.
- WP_REST_Attachments_Controller::register_routes()Registers the routes for attachments.
- WP_REST_Autosaves_Controller::register_routes()Registers the routes for autosaves.
- WP_REST_Block_Directory_Controller::register_routes()Registers the necessary REST API routes.
- WP_REST_Block_Pattern_Categories_Controller::register_routes()Registers the routes for the objects of the controller.
- WP_REST_Block_Patterns_Controller::register_routes()Registers the routes for the objects of the controller.
- WP_REST_Block_Renderer_Controller::register_routes()Registers the necessary REST API routes, one for each dynamic block.
- WP_REST_Block_Types_Controller::register_routes()Registers the routes for block types.
- WP_REST_Comments_Controller::register_routes()Registers the routes for comments.
Show all 43
- WP_REST_Edit_Site_Export_Controller::register_routes()Registers the site export route.
- WP_REST_Font_Collections_Controller::register_routes()Registers the routes for the objects of the controller.
- WP_REST_Font_Faces_Controller::register_routes()Registers the routes for posts.
- WP_REST_Global_Styles_Controller::register_routes()Registers the controllers routes.
- WP_REST_Global_Styles_Revisions_Controller::register_routes()Registers the controller's routes.
- WP_REST_Icon_Collections_Controller::register_routes()Registers the routes for the objects of the controller.
- WP_REST_Icons_Controller::register_routes()Registers the routes for the objects of the controller.
- WP_REST_Menu_Locations_Controller::register_routes()Registers the routes for the objects of the controller.
- WP_REST_Navigation_Fallback_Controller::register_routes()Registers the controllers routes.
- WP_REST_Pattern_Directory_Controller::register_routes()Registers the necessary REST API routes.
- WP_REST_Plugins_Controller::register_routes()Registers the routes for the plugins controller.
- WP_REST_Post_Statuses_Controller::register_routes()Registers the routes for post statuses.
- WP_REST_Post_Types_Controller::register_routes()Registers the routes for post types.
- WP_REST_Posts_Controller::register_routes()Registers the routes for posts.
- WP_REST_Revisions_Controller::register_routes()Registers the routes for revisions based on post types supporting revisions.
- WP_REST_Search_Controller::register_routes()Registers the routes for the search controller.
- WP_REST_Settings_Controller::register_routes()Registers the routes for the site's settings.
- WP_REST_Sidebars_Controller::register_routes()Registers the controllers routes.
- WP_REST_Site_Health_Controller::register_routes()Registers API routes.
- WP_REST_Taxonomies_Controller::register_routes()Registers the routes for taxonomies.
- WP_REST_Template_Autosaves_Controller::register_routes()Registers the routes for autosaves.
- WP_REST_Template_Revisions_Controller::register_routes()Registers the routes for revisions based on post types supporting revisions.
- WP_REST_Templates_Controller::register_routes()Registers the controllers routes.
- WP_REST_Terms_Controller::register_routes()Registers the routes for terms.
- WP_REST_Themes_Controller::register_routes()Registers the routes for themes.
- WP_REST_URL_Details_Controller::register_routes()Registers the necessary REST API routes.
- WP_REST_Users_Controller::register_routes()Registers the routes for users.
- WP_REST_View_Config_Controller::register_routes()Registers the routes for the controller.
- WP_REST_Widget_Types_Controller::register_routes()Registers the widget type routes.
- WP_REST_Widgets_Controller::register_routes()Registers the widget routes for the controller.
- WP_oEmbed_Controller::register_routes()Register the oEmbed REST API route.
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.
Signature, return type and hooks compared across 5 parsed releases.
_doing_it_wrong() notice when the required permission_callback argument is not set.from the docblock_doing_it_wrong() notice when not called on or after the rest_api_init hook.from the docblockAbout 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.