The Yoast meta description API does not exist as a write endpoint. Specifically, Yoast SEO ships its REST surface as read-only, so a POST to update a meta description silently no-ops. However, a 22-line Code Snippets PHP block solves this. Register the Yoast fields with show_in_rest=true. As a result, the standard WordPress endpoint accepts atomic writes.
This post ships the full snippet. Furthermore, it walks through the four-field atomic POST. Additionally, it addresses the indexables caveat that trips up most automation builds. As a result, you get bulk Yoast writes from Python — no extra plugins, no manual paste in WordPress admin.
Yoast does not expose its meta fields via REST by default. Specifically, _yoast_wpseo_metadesc, _yoast_wpseo_focuskw, and _yoast_wpseo_focuskeywords need a register_post_meta hook first. As a result, one Code Snippets snippet unlocks atomic writes to all four Yoast fields.
The Yoast meta description API blocker explained
Out of the box, Yoast does not expose its SEO fields to the WordPress REST endpoint. Specifically, Yoast SEO publishes a read-only REST surface. As a result, a POST to /wp/v2/posts/<id> with a meta payload returns 200 OK, but the field stays empty in the editor. The reason: Yoast does not register its post-meta keys with show_in_rest=true. Furthermore, the WordPress REST controller silently drops every meta key it does not recognize.
This catches every developer once. As a result, the first attempt at automation looks like it worked. The API call succeeded, the post saved. However, the meta description box in Yoast remains blank. Furthermore, the fix is not in the POST payload. It is one PHP hook that exposes the fields to REST. Yoast’s own developer portal confirms the REST API is read-only. In effect, it is for retrieval, not writes.
show_in_rest=true.The 22-line Code Snippets fix (full PHP block)
The fix is a single PHP snippet that registers six Yoast post-meta keys with show_in_rest=true. Specifically, the hook fires on init and loops over post types. Each Yoast field gets a string type and an edit_posts auth callback. As a result, the WordPress REST endpoint accepts Yoast writes once the snippet is active.
add_action('init', function () {
$fields = [
'_yoast_wpseo_metadesc',
'_yoast_wpseo_title',
'_yoast_wpseo_focuskw',
'_yoast_wpseo_focuskeywords',
'_yoast_wpseo_canonical',
'_yoast_wpseo_linkdex',
];
foreach (['post', 'page', 'portfolio'] as $post_type) {
foreach ($fields as $field) {
register_post_meta($post_type, $field, [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'auth_callback' => function () {
return current_user_can('edit_posts');
},
]);
}
}
});
Code Snippets is the safe path here. Furthermore, the plugin auto-deactivates any snippet that throws a PHP error at save. As a result, a typo never crashes the live site. Additionally, this is reversible in two clicks from WordPress admin. The WordPress.org plugin directory lists Code Snippets at over one million active installs. In effect, that is the trust signal most non-technical site owners need. For more on the plugin path, see the Code Snippets WordPress tutorial.
Extending the snippet to custom post types
Each custom post type needs its own register_post_meta call. Specifically, the function is per-post-type. Registering Yoast fields for post does not expose them on portfolio or any other CPT. As a result, a snippet that only covers post + page silently no-ops on a portfolio API write.
# Enumerate every CPT exposed by the REST API
GET https://example.com/wp-json/wp/v2/types
# Returns rest_base values: post, page, portfolio, attachment, product, ...
Run GET /wp-json/wp/v2/types to list every active post type on the site. The official WordPress reference covers the full parameter list. In particular, watch for plugins that register their own CPTs. WooCommerce products, Elementor templates, custom plugin types — each needs the loop. Furthermore, the snippet above handles three post types. The same pattern scales to any number — just extend the array.
Writing four Yoast fields in one Python POST
One POST request can write all four Yoast fields atomically. Specifically, the meta description, focus keyword, additional keyphrases, and SEO title live under meta. As a result, the entire SEO setup for a post lands in a single HTTP call.
import requests
from requests.auth import HTTPBasicAuth
WP_URL = "https://example.com"
auth = HTTPBasicAuth("claude-bot", "K3pW Q9mz X2dV nB7H jL4r Y6sT")
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0 Safari/537.36"
),
"Content-Type": "application/json",
}
payload = {
"meta": {
"_yoast_wpseo_metadesc": "Automate Yoast meta writes from Python in five "
"lines: register the field, then POST four values at once.",
"_yoast_wpseo_focuskw": "automate Yoast meta description",
"_yoast_wpseo_title": "Automate Yoast Meta via REST: A 22-Line Fix",
},
}
r = requests.post(
f"{WP_URL}/wp-json/wp/v2/posts/1847",
auth=auth, headers=headers, json=payload,
)
print(r.status_code, r.json().get("meta", {}))
Notice the User-Agent header. In particular, Cloudflare blocks the default python-requests user-agent with a 1010 error. As a result, a real browser User-Agent is mandatory on every request. Furthermore, the auth pattern uses an Application Password, not the WP login password. For the full auth walkthrough, see how to connect WordPress to Claude.
Yoast Premium additional keyphrases via the API
Yoast Premium’s additional keyphrases field accepts a JSON array of objects, not a flat string. Specifically, the field is _yoast_wpseo_focuskeywords (with an s). Each object needs a keyword key and a score key. As a result, the payload format trips up most first attempts. Passing a comma-separated string returns 200 OK, but the keyphrases list stays empty.
import json
additional = json.dumps([
{"keyword": "automate Yoast meta description", "score": ""},
{"keyword": "expose Yoast fields to REST", "score": ""},
{"keyword": "register_post_meta yoast", "score": ""},
])
payload = {
"meta": {
"_yoast_wpseo_focuskeywords": additional,
},
}
The score field stays empty for API writes. In particular, Yoast computes the per-keyphrase score in JavaScript at editor save. As a result, the keyphrases appear in the Yoast UI immediately. However, the score column reads empty until the post is opened and saved manually. Above all, this is the right trade-off for bulk operations. Dozens of posts each get three correctly-stored additional keyphrases in seconds. The score column populates the next time anyone clicks Update.
The indexables caveat: why the SEO score does not recompute
Yoast computes the overall SEO score in client-side JavaScript at editor save. Specifically, _yoast_wpseo_linkdex stores the green-orange-red 0 to 100 score. However, that field is only written when the block editor saves the post and Yoast’s analyzer runs. As a result, posts edited via REST API alone show empty SEO scores in the Posts list.
The workaround takes one click. Specifically, open the post in the WordPress block editor and click Update. Yoast re-runs its analyzer and writes _yoast_wpseo_linkdex to the database. Furthermore, Yoast also maintains an “indexables” table that mirrors the SEO data in an optimized form. As a result, the API write goes to wp_postmeta. Yoast’s runtime reads from indexables. The editor save reconciles the two.
The long-term fix is a Python-side Yoast scoring engine. Yoast’s analyzer is open-source at Yoast/wordpress-seo. As a result, the same algorithms can run in Python and write the score directly. Until then, the editor save remains the source of truth.
Verifying the Yoast meta description API write landed
Verification is one line of Python: read the post back and assert the meta key. Specifically, GET /wp/v2/posts/<id>?context=edit&_fields=meta returns every registered meta key. In effect, an empty value here means the snippet is not registered for that post type.
r = requests.get(
f"{WP_URL}/wp-json/wp/v2/posts/1847?context=edit&_fields=meta",
auth=auth, headers=headers,
)
meta = r.json().get("meta", {})
assert meta.get("_yoast_wpseo_metadesc"), "Snippet not active for this post type"
print("Yoast metadesc landed:", meta["_yoast_wpseo_metadesc"][:60])
Run this verification on every post pushed via the API. In particular, an empty meta._yoast_wpseo_metadesc after a 200 OK is the canary for a missing register_post_meta call. Furthermore, this check belongs in the publishing pipeline. As a result, regressions get caught once and fixed once.
| Yoast field | Meta key | Format | Plan |
|---|---|---|---|
| Meta description | _yoast_wpseo_metadesc | String | Free + Premium |
| Focus keyword | _yoast_wpseo_focuskw | String | Free + Premium |
| Additional keyphrases | _yoast_wpseo_focuskeywords | JSON array | Premium only |
| SEO title | _yoast_wpseo_title | String (with template tags) | Free + Premium |
| Canonical URL | _yoast_wpseo_canonical | URL string | Free + Premium |
| SEO score (read-only via API) | _yoast_wpseo_linkdex | Number 0-100 | JS-computed at editor save |
Want to level up your Python and WordPress automation skills?
DataCamp offers structured, hands-on courses in Python, REST APIs, and data engineering — built for practitioners. New users get a significant discount on their first subscription.
Explore DataCamp Courses →Frequently asked questions about the Yoast meta description API
Can I update Yoast meta descriptions from any HTTP client?
Yes, but not by default. Specifically, Yoast does not register its meta fields for REST exposure, so a standard POST to /wp/v2/posts/<id> with a meta payload silently no-ops. As a result, the fix is one PHP snippet that calls register_post_meta with show_in_rest=true for each Yoast field. Once the snippet is active, the same standard endpoint accepts atomic writes to the meta description, focus keyword, additional keyphrases, and SEO title.
Why does Yoast’s REST API return read-only data?
Yoast designed its REST surface for headless rendering — fetching SEO metadata to inject into a frontend, not writing it. As a result, the documented Yoast endpoints expose meta tags, schema.org JSON-LD, and Open Graph data, but no POST or PUT methods. Furthermore, write access requires a separate path: registering the underlying post-meta keys for REST yourself, then using the standard WordPress endpoint. This is intentional architecture, not a missing feature.
How do I expose _yoast_wpseo_metadesc to the REST API?
Call register_post_meta with show_in_rest=true for the field, scoped to each post type that needs API access. Specifically, the snippet uses an auth_callback that requires the edit_posts capability — Editor role qualifies, subscriber role does not. As a result, the standard WordPress REST endpoint accepts the field as part of the meta payload, and writes land in the wp_postmeta table the same way a manual edit in the Yoast meta box would.
Yoast Premium and validation questions
How do I write Yoast Premium additional keyphrases via API?
Pass a JSON-encoded array of objects to _yoast_wpseo_focuskeywords. Specifically, each object needs a keyword string and a score field — leave the score empty for API writes, since Yoast computes per-keyphrase scores in JavaScript at editor save. As a result, the additional keyphrases appear in the Yoast UI immediately; the score column populates the next time the post is opened and saved in the block editor. The field is Yoast Premium only.
Why does my meta description not save when I POST to /wp/v2/posts/?
The most common cause is a missing register_post_meta registration for that post type. Specifically, the WordPress REST controller silently drops every meta key that is not registered with show_in_rest=true — the response returns 200 OK, but the meta keys never reach the database. As a result, the symptom is identical to a silent no-op: API call succeeded, Yoast box still empty. Run GET /wp-json/wp/v2/posts/<id>?context=edit&_fields=meta to confirm whether the field is registered for that post type.
Does updating Yoast meta via REST API trigger the indexables refresh?
Partially. The meta description, focus keyword, and additional keyphrases land in wp_postmeta immediately, and Yoast’s frontend rendering picks up the changes on the next page load. However, the SEO score (_yoast_wpseo_linkdex) is computed in client-side JavaScript only when the block editor saves the post — so REST writes alone leave the score empty. As a result, the long-term fix is a Python-side Yoast analyzer; the short-term workaround is opening the post and clicking Update once.
The blocker is one missing PHP hook, not a missing REST endpoint. Specifically, register the fields, send one POST, verify with one GET. As a result, the entire SEO setup for a post takes seconds. Furthermore, the same pattern scales to bulk-fixing 50 or 500 posts in one script run. In effect, that is the workflow behind a three-hour cleanup of 1,057 issues. Above all, every post can now ship with its meta description, focus keyword, and additional keyphrases in place. Additionally, the same pattern handles Ahrefs warnings and Yoast Premium redirects from one pipeline.
— Lukas

