Five WordPress REST API primitives cover almost all SEO automation: authenticate with an application password scoped to a low-privilege role, read fields with the right context parameter, register any custom meta key with
show_in_restbefore you try to write it, verify every write by re-reading it back rather than trusting the response you just got, and know that pages and posts are different endpoints with different fields. Skip any one of these and the failure shows up quietly – a field that silently doesn’t save, a verification check that never matches, or a script that works on posts and does nothing on pages.
This site’s own publishing agent runs on exactly this stack, the same one described in more detail in what an AI SEO agent actually does. The failures further down are pulled from this site’s own operating log, not invented for the article.
Primitive one: authenticate with a scoped application password
WordPress core shipped Application Passwords in version 5.6. They’re 24-character credentials generated from a user’s profile page, passed over HTTPS using ordinary Basic Auth, and they don’t touch that user’s real login password. The detail that matters most for SEO automation: a given password inherits the full role of the account that created it. There’s no per-password scoping in core – you scope by choosing which WordPress user account generates the password, not by restricting the password itself. Set up a dedicated Editor-role (not Administrator) account for the script, generate its application password from that account’s profile, and the worst a compromised credential can do is edit content, not manage plugins or users. The full setup steps and the specific gap this leaves (this site’s own account can publish but can’t purge a plugin’s cache, which needs manage_options) are covered in this site’s application-passwords guide.
Primitive two: read with the right context
Every core REST endpoint accepts a context parameter – view, embed, or edit – and it changes which fields come back, not just how much detail they contain. The default, unauthenticated view context is what a browser or a script without credentials gets. Several fields, including password, permalink_template, and generated_slug on the Pages endpoint, only appear at all under context=edit, which requires an authenticated request. If your script reads a post back using the default context and a field you expect is simply missing from the response, that’s usually the reason, not a bug.
Primitive three: register meta before you try to write it
Custom fields – SEO plugin meta among them – don’t show up on the REST API automatically. WordPress core’s register_post_meta() function registers a meta key for a post type and accepts the same arguments as register_meta(), including show_in_rest. Set that to true for a given key, and it becomes readable and writable through the standard meta object on /wp/v2/posts, in the same request that creates or updates the post – no separate endpoint required. Skip the registration step and the API doesn’t throw an error when you try to write that key; it just silently drops it, which is the single most common reason a “successful” write leaves a field unchanged. This site’s own agent registers each of its SEO meta keys this way before ever sending a write, a pattern covered in more depth for one specific plugin in this site’s Rank Math automation piece.
Primitive four: verify by re-reading, and watch for the entity-encoding trap
A 200 response from a write request confirms WordPress accepted the call. It doesn’t confirm the value you sent is what’s actually stored, especially for anything with typographic punctuation. WordPress runs a texturize pass on rendered text fields – straight quotes and apostrophes become curly HTML entities in the rendered version of a field, even though the underlying raw value is untouched. We confirmed this live on this site’s own content while writing this piece: a post title stored as What's Different comes back from the default, unauthenticated endpoint as What’s Different in title.rendered – a byte-for-byte mismatch against the plain string that was actually written. Fetching the same post with context=edit and comparing against title.raw instead returns the exact original string. The rule that follows: never verify a write by comparing your input against a view-context rendered field. Re-read with context=edit and compare against .raw, or you’ll spend time chasing a “mismatch” that isn’t one.
Primitive five: pages and posts are not the same endpoint
It’s easy to write one client function against /wp/v2/posts and assume it works identically against /wp/v2/pages. It won’t. We pulled a single record from each endpoint on this site to check: posts carry categories, tags, format, and sticky fields that pages simply don’t have, while pages carry parent and menu_order fields that posts don’t have, reflecting their hierarchical structure instead of a flat taxonomy. A script that sets categories on what it assumes is always a post will silently do nothing if that ID happens to be a page – WordPress won’t error on an unrecognized field in most cases, it just won’t be there to set. If your SEO automation touches both content types, check type on the record first, or maintain separate write payloads for each endpoint rather than one shared shape.
The two failures that show up in production, not in docs
Two problems from this site’s own history are worth flagging because neither is a WordPress bug – both are documented, expected behavior that looks like a bug the first time you hit it. First, a bare HTTP client with no custom User-Agent header can draw an intermittent 403 from a site’s own web application firewall; this site’s Cloudflare-backed WAF has done exactly that to a plain Python urllib client with no identifying header, resolved by sending a browser-style User-Agent or switching to a tool like curl, which sets one by default. Second, a scheduled job that re-pulls a wide date range or re-writes fields it didn’t need to touch burns through the API’s own quota and load budget faster than it needs to – avoidable by fetching or writing only what actually changed since the last run, rather than the full record every time.
Putting the five together
None of these five primitives is exotic on its own – application passwords, a context parameter, one core function, a second read, and a type check. What makes REST-driven SEO automation reliable is running all five in the same script, in that order: authenticate scoped, read with intent, register before writing, verify after writing, and never assume the endpoint shape is the same across content types. Drop any one step and the failure mode is the same across all of them – quiet, not loud, and easy to miss until an audit catches a field that was never actually set. The same read-verify discipline carries over to other platform APIs this site automates against, including Search Console’s own API.
Frequently asked questions
Does my WordPress REST API script need to log in as an administrator?
No. An application password inherits the role of whichever account generated it, so a dedicated Editor-role account is enough to create, update, and publish content and its SEO meta fields. Reserve Administrator-level credentials for the handful of actions – like plugin or user management – that actually require them.
Why doesn’t the string I sent match what the REST API returns?
You’re likely comparing against a rendered field under the default view context, which runs WordPress’s typography pass and converts straight quotes and apostrophes into HTML entities. Re-fetch the record with context=edit and compare against the corresponding .raw field instead, which returns the exact string that was stored.
Why does a REST API write to a custom field sometimes do nothing?
The meta key almost certainly hasn’t been registered for REST access. WordPress core’s register_post_meta() function needs to be called with show_in_rest set to true for a given key before that key will accept writes through the API’s meta object; until then, the API accepts the request without error but drops the unregistered field.
Can I use the same code to update WordPress pages and posts?
Not without changes. The Pages and Posts REST endpoints return different schemas – posts include categories, tags, format, and sticky, while pages include parent and menu_order instead. Check the type field on a record before applying a taxonomy-style update, or maintain separate payload shapes for each endpoint.

Leave a Reply