You send a meta description to a post’s REST API endpoint, WordPress replies with HTTP 200, and the field in the Rank Math panel is still empty. One function call per field fixes the write. Done the obvious way, it also publishes every focus keyword you chose.
rank_math_title, rank_math_description, rank_math_focus_keyword), but it does not register them for the REST API. Until you do, a REST request that sets them returns 200 and saves nothing. Register each key with register_post_meta() using show_in_rest => true and an auth_callback, and writes work against the very keys the Rank Math panel reads. The catch is on the other side: show_in_rest also makes those fields readable by anyone in the post’s REST record, because auth_callback only guards writes. A small rest_prepare_post filter hides them from visitors while editors and scripts keep full access.Table of Contents
Who runs into this
Anyone who creates or updates posts from outside the editor: a migration script, a headless front end, a content importer, an integration that syncs posts from a spreadsheet or another CMS. The title and body go through the REST API without trouble, so it is natural to send the SEO title, meta description and focus keyword the same way. Setting them by hand afterwards in the Rank Math panel defeats the point of automating, and leaves no record of what was set.
The write that returns 200 and saves nothing
Out of the box, this request looks like it works:
curl -u editor:APP-PASSWORD -X POST https://example.com/wp-json/wp/v2/posts/123 \
-H "Content-Type: application/json" \
-d '{"meta": {"rank_math_description": "A description written over REST"}}'
Code language: Bash (bash)
It returns HTTP 200 and the full post. We reproduced it on a test site, as an administrator, and checked the database afterwards:
unregistered key, admin write: HTTP 200
stored value: '' (nothing saved)
key in the response's meta: noCode language: JavaScript (javascript)
The REST API only writes meta keys that have been registered for it. Anything else in the meta object is dropped without an error, so the only sign of failure is a field that never changes. A 200 there means the request was understood, not that your value was stored, which is the same trap as treating a success message as confirmation.
A REST write that returns 200 has told you it understood the request, not that it saved your field.
Register the keys Rank Math already uses
Registration is one call per key, at init:
add_action( 'init', function () {
$keys = array(
'rank_math_focus_keyword' => 'Primary keyword first, then secondaries, comma-separated.',
'rank_math_description' => 'The meta description in the page head.',
'rank_math_title' => 'The SEO title, when it differs from the post title.',
'rank_math_pillar_content' => 'Pillar content flag: the string "on" when set.',
);
foreach ( $keys as $key => $description ) {
register_post_meta( 'post', $key, array(
'type' => 'string',
'single' => true,
'default' => '',
'description' => $description,
'show_in_rest' => true,
'sanitize_callback' => 'wp_strip_all_tags',
// Writable by anyone who could already type it into the Rank Math panel.
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
) );
}
}, 5 );
Code language: PHP (php)
Three choices in there are deliberate:
- The same keys, not new ones. These are the meta keys Rank Math itself reads and writes, so a value set over REST is the value its panel shows, and the next script run sees an edit made in the panel. On a test site, a meta description written this way appeared word for word in the published page’s
<meta name="description">tag. - A closed list, not every
rank_math_*key. Rank Math keeps schema and redirect internals under the same prefix. Nothing in a publishing script should be writing those, so they stay unregistered. auth_callbackatedit_posts. The bar is “could this user have typed it into the panel anyway”. In a test, a subscriber who tried to write the description got a 403 (rest_cannot_edit) and the stored value didn’t move.
With that in place, the same curl request stores the value, and the post’s REST response shows it under meta.
The part most tutorials skip: show_in_rest is public on read
It is easy to assume the fields stay behind login, since you needed to authenticate to write them. Request a published post from the REST API without logging in, though, and they are right there:
GET /wp-json/wp/v2/posts/<id>?_fields=meta (no authentication)
rank_math_focus_keyword = "<the primary keyword>, <three secondaries>"
rank_math_description = "<the meta description>"
rank_math_title = ""
rank_math_pillar_content = ""Code language: HTML, XML (xml)
show_in_rest => true adds the field to the post’s public REST representation. auth_callback is consulted when someone tries to change the value, not when someone reads it. A clean test install showed the same: an anonymous read returned all three keys, focus keyword included.
Most of that is harmless. The meta description and SEO title are in the page head for anyone to see. The focus keyword is different: it is your own note about which search you are trying to win, it appears nowhere on the page, and a single request to /wp-json/wp/v2/posts?per_page=100 hands over a hundred of them.
auth_callback decides who may change a meta field. It has no say in who may read it.
Keep them private with one filter
Registration has to stay public-capable for writes to work, so the fix goes on the response instead. This filter removes the keys from a post’s REST output for anyone who couldn’t edit that post. It hooks rest_prepare_post, the documented way to change a REST response:
add_filter( 'rest_prepare_post', function ( $response, $post ) {
if ( current_user_can( 'edit_post', $post->ID ) ) {
return $response; // editors and authenticated scripts keep everything
}
$data = $response->get_data();
foreach ( array( 'rank_math_focus_keyword', 'rank_math_description', 'rank_math_title', 'rank_math_pillar_content' ) as $key ) {
unset( $data['meta'][ $key ] );
}
$response->set_data( $data );
return $response;
}, 10, 2 );
Code language: PHP (php)
It checks the capability rather than the request’s context, so an editor’s script reading in the default view context still gets the values back. On the test site, with the filter added:
anonymous read: meta {} (all keys gone)
admin read (edit context): focus keyword, description and title present
admin write (rank_math_title): HTTP 200, storedCode language: CSS (css)
To confirm it on your own site, repeat the anonymous ?_fields=meta request from a private browser window or with curl and no credentials: no rank_math_* key should come back, while your authenticated script still reads and writes them.
If you register meta for other post types (pages, products), add the same filter for each one: rest_prepare_page, rest_prepare_product and so on.
The short version
- Rank Math’s SEO fields are plain post meta under
rank_math_title,rank_math_description,rank_math_focus_keywordandrank_math_pillar_content. - They are not registered for the REST API, so writing them returns 200 and stores nothing.
register_post_meta()withshow_in_restand anauth_callbackmakes them writable; register a closed list, not the whole prefix.show_in_restalso makes them readable by anyone.auth_callbackguards writes only, so every focus keyword lands in each published post’s public JSON.- A
rest_prepare_postfilter that strips the keys for users who can’t edit the post keeps them private, without breaking writes or editors’ reads.
Frequently Asked Questions
Why does my REST request return 200 but the field doesn’t change?
Because the meta key isn’t registered for the REST API. WordPress ignores unregistered keys in the meta object without raising an error, so the request succeeds and nothing is saved. Read the field back after writing it; if it’s empty, register the key.
Which meta keys does Rank Math use for the SEO title, description and focus keyword?
rank_math_title for the SEO title, rank_math_description for the meta description and rank_math_focus_keyword for the focus keyword, where the first comma-separated term is the primary keyword. The pillar content checkbox is rank_math_pillar_content, stored as the string “on” when ticked.
Does this work for pages and custom post types?
Yes, but each type needs its own registration. The first argument of register_post_meta() is the post type, so register the keys for page (or your custom type) as well as post, and add the matching rest_prepare_<type> filter. A custom post type must also support custom-fields, or its REST response has no meta field to write to.
Will the privacy filter break the block editor?
No. The editor loads posts as a logged-in user who can edit them, so the capability check passes and the fields come back unchanged. Only requests from users who can’t edit the post lose them.
Should I register every rank_math_ key at once?
No. Register only the fields your script actually sets. Rank Math stores schema and redirect data under the same prefix, and opening those to REST writes invites a script to overwrite something it doesn’t own.