A WordPress plugin that exposes navigation menus via custom REST API endpoints. Perfect for headless WordPress, decoupled applications, and external integrations.
- β Expose all WordPress menus via REST API
- β
Get menus by ID, slug, or location (e.g.,
primary,footer) - β
Nested menu hierarchy support with
?nested=true - β
Rich item meta with
?meta=true(description, featured image, excerpt) - β
Field filtering with
?fields=id,title,urlfor lean payloads - β List all registered menu locations
- β Built-in response caching (auto-flushed when menus change)
- β Admin settings page (Settings β EndPointy Menus)
- β Optional API-key authentication + CORS allow-list
- β
Developer filter hook (
endpointy_menus_item) - β Clean, structured JSON output
- β Lightweight and performant
- Download or clone this repository
- Copy the
endpointy-menusfolder to your WordPresswp-content/pluginsdirectory - Activate the plugin from WP Admin β Plugins
- Ensure you have menus configured under Appearance β Menus
Base namespace: endpointy-menus/v1
GET /wp-json/endpointy-menus/v1/menus
Returns all registered menus with their locations and items.
Example Response:
[
{
"id": 2,
"name": "Main Menu",
"slug": "main-menu",
"locations": ["primary"],
"items": [...]
}
]GET /wp-json/endpointy-menus/v1/menus/{id}
Returns a specific menu and its items.
Example:
GET /wp-json/endpointy-menus/v1/menus/2
GET /wp-json/endpointy-menus/v1/menus/slug/{slug}
Returns a specific menu and its items by slug.
Example:
GET /wp-json/endpointy-menus/v1/menus/slug/main-menu
GET /wp-json/endpointy-menus/v1/locations
Returns all registered menu locations with assigned menus.
Example Response:
[
{
"location": "primary",
"description": "Primary Menu",
"menu_id": 2,
"menu_name": "Main Menu"
}
]GET /wp-json/endpointy-menus/v1/locations/{location}
Returns the menu assigned to a specific location.
Example:
GET /wp-json/endpointy-menus/v1/locations/primary
Add ?nested=true to any menu endpoint to get a hierarchical tree structure:
GET /wp-json/endpointy-menus/v1/menus/2?nested=true
GET /wp-json/endpointy-menus/v1/locations/primary?nested=true
Flat structure (default):
{
"items": [
{"id": 1, "title": "Home", "parent": 0},
{"id": 2, "title": "About", "parent": 0},
{"id": 3, "title": "Team", "parent": 2}
]
}Nested structure (?nested=true):
{
"items": [
{
"id": 1,
"title": "Home",
"children": []
},
{
"id": 2,
"title": "About",
"children": [
{
"id": 3,
"title": "Team",
"children": []
}
]
}
]
}All menu endpoints accept these optional parameters (combinable):
| Param | Example | Description |
|---|---|---|
nested |
?nested=true |
Hierarchical tree with children arrays |
meta |
?meta=true |
Adds description, attr_title, current, featured_image, excerpt |
fields |
?fields=id,title,url |
Return only the listed item fields (children always kept in nested mode) |
GET /wp-json/endpointy-menus/v1/menus/2?nested=true&meta=true&fields=id,title,url,featured_image,children
A settings page lives at Settings β EndPointy Menus:
- Response caching β menu responses are cached as transients with a configurable lifetime and automatically flushed whenever a menu is edited or deleted.
- API key β turn on Require API key to protect the endpoints. Send the key as an
X-API-Keyheader or anapi_keyquery parameter:curl -H "X-API-Key: YOUR_KEY" https://your-site.com/wp-json/endpointy-menus/v1/menus - CORS β add allowed origins (one per line, or
*) to sendAccess-Control-Allow-Originheaders for browser clients. - Rate limiting β cap requests per minute per IP (0 = off). Responses include
X-RateLimit-Limit/X-RateLimit-Remaining; over-limit requests get429withRetry-After. - ETag / Last-Modified β conditional-request support. Send
If-None-MatchorIf-Modified-Sinceand unchanged menus return304 Not Modified:curl -H 'If-None-Match: "<etag>"' https://your-site.com/wp-json/endpointy-menus/v1/menus
// Add or modify fields on every menu item.
add_filter( 'endpointy_menus_item', function ( $data, $item, $meta ) {
$data['acf'] = get_fields( $item );
return $data;
}, 10, 3 );Each menu item includes:
| Property | Type | Description |
|---|---|---|
id |
int | Menu item ID |
title |
string | Display title |
url |
string | Link URL |
parent |
int | Parent item ID (0 for top-level) |
order |
int | Menu order |
type |
string | Item type (post_type, taxonomy, custom, etc.) |
object |
string | Object type (page, post, category, etc.) |
object_id |
int | ID of the linked object |
target |
string | Link target (_blank, etc.) |
classes |
array | CSS classes |
xfn |
string | XFN relationship |
children |
array | Child items (only in nested mode) |
description |
string | Item description (only with meta=true) |
attr_title |
string | Title attribute (only with meta=true) |
current |
bool | Whether the item is the current page (only with meta=true) |
featured_image |
string|null | Featured image URL for post-type items (only with meta=true) |
excerpt |
string | Post excerpt for post-type items (only with meta=true) |
// Get primary menu
fetch('https://your-site.com/wp-json/endpointy-menus/v1/locations/primary')
.then(response => response.json())
.then(menu => console.log(menu));
// Get nested menu structure
fetch('https://your-site.com/wp-json/endpointy-menus/v1/menus/2?nested=true')
.then(response => response.json())
.then(menu => console.log(menu));import { useEffect, useState } from 'react';
function Navigation() {
const [menu, setMenu] = useState(null);
useEffect(() => {
fetch('https://your-site.com/wp-json/endpointy-menus/v1/locations/primary?nested=true')
.then(res => res.json())
.then(data => setMenu(data));
}, []);
if (!menu) return <div>Loading...</div>;
return (
<nav>
{menu.items.map(item => (
<a key={item.id} href={item.url}>{item.title}</a>
))}
</nav>
);
}# Get all menus
curl https://your-site.com/wp-json/endpointy-menus/v1/menus
# Get menu by location
curl https://your-site.com/wp-json/endpointy-menus/v1/locations/primary
# Get nested structure
curl "https://your-site.com/wp-json/endpointy-menus/v1/menus/2?nested=true"- WordPress 5.0+
- PHP 7.0+
endpointy-menus/
βββ endpointy-menus.php # Main plugin file
βββ readme.txt # WordPress.org readme
βββ README.md # GitHub readme
GPL v2 or later
Gunjan Jaswal
- Website: https://gunjanjaswal.me
- GitHub: https://github.com/gunjanjaswal/Endpointy-Menus
If you find this plugin useful, consider supporting the developer:
- Added admin settings page (Settings β EndPointy Menus).
- Added response caching with configurable lifetime and auto-flush on menu changes.
- Added optional API-key authentication and configurable CORS allow-list.
- Added per-IP rate limiting with
X-RateLimit-*headers and429responses. - Added ETag / Last-Modified conditional requests (
304 Not Modified). - Added
/menus/slug/<slug>endpoint. - Added
meta=trueparameter (description, featured image, excerpt, current flag). - Added
fields=parameter for partial responses. - Added
endpointy_menus_itemdeveloper filter. - Added menu
countand locationmenu_slugto responses.
- Updated "Tested up to" to WordPress 7.0.
- Updated donation link to Ko-fi (https://ko-fi.com/gunjanjaswal).
- Added support for filtering menus by location.
- Added nested menu hierarchy with
nested=truequery parameter. - Added
/locationsendpoint to list all menu locations. - Added
/locations/<location>endpoint to get menu by location.
- Initial release.
Contributions, issues, and feature requests are welcome! Feel free to check the issues page.
Give a βοΈ if this project helped you!
Made with β€οΈ by Gunjan Jaswal