Skip to main content

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.

EndpointMethodPurpose
/wp-json/reactwp/v1/bootstrapGETPublic first-load payload for external apps
/wp-json/reactwp/v1/route?view=/about/GETPublic route payload. The view parameter is required.
/wp-json/reactwp/v1/navigationGETAll normalized menus
/wp-json/reactwp/v1/navigation?location=primaryGETOne normalized menu location
/wp-json/reactwp/v1/settingsGETPublic settings only
/wp-json/reactwp/v1/sitemapGETPublic route index
/wp-json/reactwp/v1/preview?postId=42&token=...GETSigned preview payload
/wp-json/reactwp/v1/auth/meGETCurrent authenticated user
/wp-json/reactwp/v1/auth/loginPOSTCookie login for headless frontends
/wp-json/reactwp/v1/auth/logoutPOSTCookie logout

Public Contract

Headless responses include:

  • apiVersion
  • generatedAt
  • endpoint-specific payload data

The public route shape includes:

  • id
  • type
  • template
  • status
  • lang
  • title
  • pageName
  • path
  • search
  • query
  • url
  • seo
  • mediaGroups
  • data
  • head
  • is404
  • links

Use this page as the source of truth when wiring a Next, Astro, Remix, or Vite frontend.

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_origins filter

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.

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);

Then send the external frontend to a URL that can request:

/wp-json/reactwp/v1/preview?postId=42&token=...

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.0",
"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.