I'm trying to get the navigation menu from JSON response using WP REST API v2 plugin.

There is no navigation menu plugin extension for the REST API v2, but only for V1.

From codex WordPress Post Types, I learned that the navigation menu is treated as a post type.

From Rest API Doc, this is how we get posts of a type:

GET http://demo.wp-api.org/wp-json/wp/v2/types/<type>

I tried to get it like so:

URL : http://localhost/wptest/wp-json/wp/v2/types/nav_menu_item

I received 403 error.

{"code":"rest_cannot_read_type","message":"Cannot view type.","data":{"status":403}}

the server understood my request but it refused to give the data.

Q: How can I fix this?

  • All of these answers are just terrible. Install this, extend that. It should already be built-in, the community should open an issue on GitHub. Dec 20, 2018 at 1:16

7 Answers 7


There is a navigation menu plugin extension for the REST API v2: https://wordpress.org/plugins/wp-api-menus/

  • 1
    I just visited the link and found that the plugin, unfortunately, hasn't been updated for 3 years. Mar 26, 2020 at 0:48

Since I don't like it myself when the top answer is "Install plugin X", here is how I solved it:

Menus are currently not available in the WP Rest. So what you need to do is register your own custom endpoint and then just call that route from your application that needs it.

So you would include something like this (in your functions.php, plugin, wherever):

function get_menu() {
    # Change 'menu' to your own navigation slug.
    return wp_get_nav_menu_items('menu');

add_action( 'rest_api_init', function () {
        register_rest_route( 'myroutes', '/menu', array(
        'methods' => 'GET',
        'callback' => 'get_menu',
    ) );
} );

For the example above you would access the data from:


You can use the above method to create any routes you like to get any type of data that is not available in the WP Rest. Also good if you need to process some data before sending it to your application.

  • 5
    thanks for sharing your workaround, with more than just a plugin link ;-) It would be better though to prefix your function names or use namespace, to avoid possible name collision, as get_menu() is pretty general.
    – birgire
    Jul 24, 2017 at 21:30
  • Awesome, people doesn't realize that most of the people have already from 30 to 70 plugins already installed. They even have plugins to keep deactivated others! it is crazy. I think i am going to install a plugin to keep this thread. Jul 9, 2018 at 22:49
  • it does only output false
    – moeses
    Nov 16, 2018 at 20:57
  • @moeses just make sure the name of the menu in menu setting at wp-admin is the same name you are passing as parameter into wp_get_nav_menu_items function Aug 26, 2021 at 13:43
  • @Liren, is there a way i can protect this route? so i can only call from files from my wordpress aplication? protecting from anyone who may discover the route? Aug 26, 2021 at 13:45

@Liren answer works well. However few beginners may not able to adjust the route. Here is the code that works well with WordPress Rest API v2 with minimum modification.

Replace your menu name only in wp_get_nav_menu_items() function. If menu name and slug does not work (Return false), use Menu ID (visible in Dashboard while editing that Menu).

function get_my_menu() {
    // Replace your menu name, slug or ID carefully
    return wp_get_nav_menu_items('Main Navigation');

add_action( 'rest_api_init', function () {
    register_rest_route( 'wp/v2', 'menu', array(
        'methods' => 'GET',
        'callback' => 'get_my_menu',
    ) );
} );

Route URL:


More details covered in Tutorial: WordPress Rest API – Get Navigational Menu Items

  • It's good solution for have only one route Jan 23, 2020 at 16:40

I opted for the simplest way to do it, still keeping it dynamic so that you can fetch more than one menu.

 * Returns menu items in a array based on the navigation menu id passed
 * @param object The actual request where parameters can be accessed.
 * @return array The menu items contained in that specific menu
function expose_navigation($request) {
  $id = $request['id'];
  return wp_get_nav_menu_items($id);

 * Exposes under /navigation/{id} the menu items in the wp-json api
 * @return void
function expose_navigation_to_rest() {
  register_rest_route( 'wp/v2', '/navigation/(?P<id>\d+)', [
      'methods' => 'GET',
      'callback' => 'expose_navigation'

add_action('rest_api_init', 'expose_navigation_to_rest');

So that is easily queriable like /navigation/{id}


You need to add 'show_in_rest' => true, while registering post type.

See details here http://v2.wp-api.org/extending/custom-content-types/

  • 2
    register_post_type('nav_menu_item', array('show_in_rest' => true));
    – luukvhoudt
    Nov 28, 2019 at 1:24
  • This would be a great solution, but for a reason that is not obvious to me the data in the response does not contain the menu information, there is no title, no description or anything useful (for me) in the response. Am I missing something?
    – Johannes
    Nov 15, 2020 at 0:37

I agree with @Lirens answer, but menus should be called by ID, not slug. Also, the slash before the menu path is not needed. So it becomes something more like this:

function get_menu() {
    # Change '2' to your own navigation ID.
    return wp_get_nav_menu_items(2);

add_action( 'rest_api_init', function () {
    register_rest_route( 'myroutes', 'menu', array(
        'methods' => 'GET',
        'callback' => 'get_menu',
    ) );
} );

Like this it worked for me.


I don't think a plugin should be used for these kind of tasks. Also hkc's answer is actually not that bad, it only needs some further explanation to make this work with the nav_menu_item post type (the one used for wp navigation menus).

This post type is already registered and thus we need to alter it, this is easily done by hooking into the register_post_type_args filter. This filter allows us to change the arguments for a specific post type. The code below shows just that for the nav_menu_item post type.

add_filter('register_post_type_args', function ($args, $post_type) {
    if ($post_type == 'nav_menu_item' &&
        class_exists('WP_REST_Posts_Controller') &&
        !class_exists('WP_REST_NavMenuItem_Controller')) {

        class WP_REST_NavMenuItem_Controller extends WP_REST_Posts_Controller {
            public function get_items( $request ) {
                $args = wp_parse_args($request, [
                    'order' => 'ASC',
                    'orderby' => 'menu_order',

                $output = [];

                if (empty($request['menu'])) {
                    $menus = get_registered_nav_menus();

                    foreach ( $menus as $location => $description ) {
                        $items = wp_get_nav_menu_items($location, $args);
                        $output = array_merge($output, is_array($items) ? $items : []);
                } else {
                    $items = wp_get_nav_menu_items($request['menu'], $args);
                    $output = array_merge($output, is_array($items) ? $items : []);

                return rest_ensure_response($output);

            public function get_collection_params() {
                $query_params = parent::get_collection_params();
                $query_params['menu'] = [
                    'description' => __( 'The name or also known as theme_location of the menu' ),
                    'type' => 'string',
                return $query_params;

        // Alter the post type arguments
        $args['show_in_rest'] = true;
        $args['rest_controller_class'] = 'WP_REST_NavMenuItem_Controller';
    return $args;
}, 10, 2);

As you might have noticed from the code above, the code does a little bit more than just showing the post type in the REST. It also alters the default Posts REST controller to show a somewhat similar output in the REST as described in Liren's answer. Although next to that it also does what all post type REST controllers do and thus gives you more control and functionality. Also consider this as a more stable options as it wouldn't conflict with other REST routes and last but not least, it also way more convenient to work with.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.