Headless API
Role
ReactWP can be consumed by an external frontend without replacing the integrated WordPress + React mode.
The headless API is a public contract layered over the same runtime that powers the theme shell:
- WordPress still resolves content, menus, settings, SEO, and ACF data
- external frontends receive normalized JSON
- authenticated requests still run inside WordPress, so callbacks can use
wp_get_current_user()
Endpoints
All endpoints use the reactwp/v1 REST namespace.
| Endpoint | Method | Purpose |
|---|---|---|
/wp-json/reactwp/v1/bootstrap | GET | Public first-load payload for external apps |
/wp-json/reactwp/v1/route?view=/about/ | GET | Public route payload. The view parameter is required. |
/wp-json/reactwp/v1/navigation | GET | All normalized menus |
/wp-json/reactwp/v1/navigation?location=primary | GET | One normalized menu location |
/wp-json/reactwp/v1/settings | GET | Public settings only |
/wp-json/reactwp/v1/sitemap | GET | Public route index |
/wp-json/reactwp/v1/preview?postId=42 | GET or POST | Signed preview payload; send the token in a request header |
/wp-json/reactwp/v1/auth/me | GET | Current authenticated user |
/wp-json/reactwp/v1/auth/login | POST | Cookie login for headless frontends |
/wp-json/reactwp/v1/auth/logout | POST | Cookie logout |
Public Contract
Headless responses include:
apiVersiongeneratedAt- endpoint-specific payload data
The public route shape includes:
idtypetemplatestatuslangtitlepageNamepathsearchqueryurlseomediaGroupsdataheadis404linksrender
The current public contract version is 1.4. Rendering metadata is useful to integrated tooling and optional to external frontends, which still own their own rendering strategy.
The public bootstrap does not include currentUser. This keeps its payload cacheable and prevents a shared response from mixing public content with one WordPress session. Use the no-store /auth/me endpoint whenever the external frontend needs identity, roles, capabilities, or a REST nonce.
Public bootstrap and route resolution return a 404 payload for drafts, private posts, scheduled posts, trashed posts, password-protected posts, and post types that WordPress does not consider publicly viewable. A valid signed preview token is the explicit exception for unpublished content.
Use this page as the source of truth when wiring a Next, Astro, Remix, or Vite frontend.
Do not consume the inline #reactwp-bootstrap payload from an external application. That is the integrated-theme contract and can contain project-specific internal values. The public endpoints pass through PublicPayload and add the stable response metadata intended for external consumers.
Current Language
Public bootstrap responses expose site.language and site.locale. Every normalized route exposes route.lang.
- use
route.langas the current route language - use
site.languageas the bootstrap fallback - use
site.localefor the complete WordPress locale, such asfr_CA
const language = payload.route.lang || payload.site.language || 'en';
The bootstrap, navigation, settings, sitemap, and preview endpoints accept a sanitized lang parameter for multilingual integrations. ReactWP switches Polylang when available and fires the WPML language-switch action before constructing those responses:
GET /wp-json/reactwp/v1/bootstrap?lang=fr&view=/fr/a-propos/
GET /wp-json/reactwp/v1/navigation?lang=fr&location=primary
The route endpoint resolves the localized permalink supplied in its required view parameter and returns the resolved language in route.lang.
CORS Allowlist
ReactWP does not use * for headless authentication.
Allowed origins come from:
- the WordPress site URL
- the WordPress home URL
- Site settings > Headless API > Allowed Headless Origins
- the
rwp_headless_allowed_originsfilter
Example:
add_filter('rwp_headless_allowed_origins', function($origins){
$origins[] = 'https://app.example.com';
$origins[] = 'http://localhost:3000';
return $origins;
});
For authenticated browser requests, the frontend origin must be allowlisted and should use HTTPS. Localhost origins are allowed for local development.
Login Flow
External browser frontends can create a WordPress session with:
const response = await fetch('https://cms.example.com/wp-json/reactwp/v1/auth/login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'editor@example.com',
password: 'password',
remember: true
})
});
const { currentUser } = await response.json();
When login succeeds, ReactWP returns:
- the current user payload
- a
restNonce - WordPress auth cookies set by the CMS domain
Subsequent authenticated requests should include both the cookies and nonce:
await fetch('https://cms.example.com/wp-json/my-app/v1/private-data', {
credentials: 'include',
headers: {
'X-WP-Nonce': currentUser.restNonce
}
});
Inside the WordPress REST callback, normal WordPress user APIs work:
register_rest_route('my-app/v1', '/private-data', [
'methods' => 'GET',
'permission_callback' => function(){
return is_user_logged_in();
},
'callback' => function(){
$user = wp_get_current_user();
return [
'userId' => $user->ID,
'displayName' => $user->display_name,
];
},
]);
The route must also be allowed through ReactWP's REST allowlist if it is used outside the admin REST surface:
add_filter('rwp_allowed_rest_routes', function($routes){
$routes[] = '/my-app/v1/private-data';
return $routes;
});
Logout Flow
Logout requires the REST nonce when a user is logged in:
await fetch('https://cms.example.com/wp-json/reactwp/v1/auth/logout', {
method: 'POST',
credentials: 'include',
headers: {
'X-WP-Nonce': currentUser.restNonce
}
});
Security Notes
ReactWP's headless auth is designed for browser-to-CMS requests.
Important defaults:
- login responses never reveal whether the username or password was wrong
- repeated failed logins are rate-limited
- authenticated origins are allowlisted
- preview access uses signed, expiring tokens
- auth and preview responses send no-store cache headers
- public settings are opt-in through
rwp_headless_public_settings - user capabilities are opt-in through
rwp_headless_user_capabilities
For production cross-origin cookie auth, serve the CMS over HTTPS and make sure your cookie policy supports the deployment topology. Same-site subdomain setups are usually simpler than unrelated domains.
ReactWP's global REST gate still applies around project endpoints. Public ReactWP endpoints are allowlisted by the runtime. A custom external endpoint must be allowed through rwp_allowed_rest_routes and must keep an appropriate permission_callback of its own.
Preview Tokens
Generate preview tokens server-side:
$token = rwp::preview_token($post_id, 600);
Or use the runtime class directly:
$token = ReactWP\Runtime\PreviewToken::create($post_id, 600);
Request the preview with the token in X-ReactWP-Preview-Token:
const response = await fetch(
'https://cms.example.com/wp-json/reactwp/v1/preview?postId=42',
{
headers: {
'X-ReactWP-Preview-Token': token
}
}
);
Authorization: Bearer <token> is also supported. Query-string preview tokens are disabled by default because URLs can be retained in logs, browser history, and analytics; rwp_headless_allow_preview_query_token is available only for a reviewed legacy integration.
Tokens are signed with WordPress salts, scoped to one post ID, and expire after the configured TTL.
Public Settings
The settings endpoint exposes nothing by default.
Initial response:
{
"apiVersion": "1.4",
"generatedAt": "...",
"settings": {}
}
Add project-specific public settings with:
add_filter('rwp_headless_public_settings', function($settings){
$settings['featureFlags'] = [
'accountArea' => true,
];
return $settings;
});
Do not expose secrets, private option values, API keys, or unfiltered admin configuration through this filter.
Current User Payload
By default, auth/me returns a minimal current-user payload.
To expose a small capability map:
add_filter('rwp_headless_user_capabilities', function($capabilities, $user){
return [
'readPrivateArea' => user_can($user, 'read'),
'editPosts' => user_can($user, 'edit_posts'),
];
}, 10, 2);
Keep this payload small and intentional. Do not send the full WordPress capabilities array to the browser unless the project truly requires it.
Sitemap Customization
The sitemap defaults to public WordPress content and is bounded to a maximum of 1000 items per response.
Customize it with:
rwp_headless_sitemap_post_typesrwp_headless_sitemap_limitrwp_headless_sitemap_items
See Hooks and Filters.
Caching Responses
Auth, login, logout, and preview payloads use no-store behavior. Do not cache those responses at a CDN.
Public route, bootstrap, navigation, settings, and sitemap responses can be cached according to project infrastructure, but a project must define an invalidation strategy when WordPress content changes. The integrated ReactWP browser generation does not automatically purge an external frontend's CDN.