Skip to content

What a 200 from the WordPress REST API does not tell you

An HTTP 200 response does not prove a WordPress post was saved or published correctly. Verify the response, saved record, and public page independently.

Published
Updated
AI-generated, checked by independent automated gates, no human editorial review.
How this guide was made

AI-generated. Written by an automated system, checked by independent automated gates, and published without human editorial review. How we label and verify.

On 14 August a retry agent was working through a batch edit against our own live site over the WordPress REST API. On one published article it wrote its own escaping test string into the content field. The whole body was replaced by a short diagnostic fragment the agent had been using to check its own quoting. The POST returned 200. The JSON validated. The agent finished the group and reported that it had completed successfully.

We got the article back only because an unrelated task earlier that week had happened to leave a copy of the body on disk. Nothing in the procedure put it there. We were lucky exactly once.

What we thought we were verifying

Our checks at the time were a status code and a schema validation, and we treated the pair as confirmation that the edit had landed as intended. Both checks are real and both go green when things go well. They just answer a different question than the one that mattered.

A status code reports transport: the request arrived, the server accepted it, the handler did not throw. A schema validator reports syntax: the payload was shaped the way the endpoint expects. Neither has any opinion about whether the bytes in the body are the bytes you meant to send. The agent’s own summary was worse than useless — it described the agent’s intent from inside the agent’s own context, the one vantage point from which this error is invisible.

Why the WordPress REST API makes this particular write dangerous

The mechanism is not exotic. The content field is destructive: it replaces, it does not add. No partial writes, no merge, no append mode. Whatever you transmit becomes the entire stored body, and a short string is as valid as a long one. An escaping test fragment is a perfectly legal article as far as the endpoint is concerned.

Stack that against what a status field can express. A 200 means the write completed. It cannot mean what is now stored is what you sent, because the endpoint has no access to your intent — it only has the payload. The one distinction you need, between “saved” and “saved as something other than what I sent,” is precisely the one no status field can draw. Asking for it is a category error, not a missing feature.

There is a second edge on the same blade. The WordPress REST API can hand you a rendered representation or a raw one, and the rendered form has already passed through display filters. It is not round-trippable: read it, edit it, write it back, and the stored markup is corrupted while every check you own still reports success.

Parallel agents make it strictly worse

Run several agents against the same post and this stops being an accident of one bad payload. Each reads, builds its version of the body, and writes. There is no compare-and-set, so the last write wins and every agent receives its own 200. A body overwritten by a sibling seconds ago is indistinguishable, from inside any one agent, from a body it wrote itself. The batch reports full success and the article is whatever finished last.

Not every write through the WordPress REST API is the same risk

The corollary matters as much as the warning. A write that transmits only scalar fields — a title, a slug, a status, an excerpt — and never transmits content at all is in a different risk class entirely. A field you never send cannot be replaced by what you did not send.

Wrapping a one-field change in the full body-write discipline is not extra safety, it is cost with nothing on the other side of it. We measured it: applying the heavy loop to a single-field change consumed 99,749 tokens against a 59,104-token baseline that reached the identical verified result. Ceremony beyond the risk is not caution. So the first decision in any write is which of the two you are doing — merely expensive to get wrong in one direction, destructive in the other.

What follows for anyone building this

What matters here is not a checklist but an ordering. When the previous value is the only copy that exists, reading it first is not caution — it is part of the write operation. A write that cannot produce the prior state on demand is not a safe write that went well; it is an unsafe write that has not failed yet.

The rest follows. Verification runs against stored bytes — lengths, opening and closing characters, the structural blocks you intended, whether any appears twice — and never against the platform’s success flag. The read-back has to come from something that did not perform the write, because a process confirming its own work is confirming its intent, not the result. A body far below a plausible length floor is wiped, not short. And the window in which the original is recoverable closes at the next write, which is why restoring comes before investigating.

The runnable procedure, with its guards and the checkpoint that lets a third party re-derive the verdict, lives in the member library.

How we know

What the WordPress documentation says

Most of the mechanism above is documented, just spread across pages that rarely get read together. The posts endpoint takes a context argument, described in the reference as the Scope under which the request is made; determines fields present in response. The default is view. That default matters, because the stored source of a post is not what a view request gives you. An XWP engineering write-up on the API’s design explains that in the content object the raw field only belongs to the edit context, while the rendered field appears in both. An agent that edits stored content should read and write the raw field, never round-trip the rendered output.

You can also ask for less. The handbook’s global parameters page says that to return only a subset of the fields in a response you can pass _fields. For a read-back that is useful twice over: the response is smaller, and it contains only the field you are about to compare.

Authentication is the other half. Application passwords arrived in WordPress 5.6 as a new credential type, and the integration guide says those credentials can be passed along to REST API requests served over https:// using Basic Auth, and the admin documentation says they are typically used via HTTP Basic Authentication. Nothing about that scheme narrows what a request may do. An agent holding an application password writes with whatever reach that account already has.

Revisions are the safety net people assume they have. The WordPress documentation states that revisions are stored in the posts table, and that a site can cap how many are kept per post with the WP_POST_REVISIONS constant or the wp_revisions_to_keep filter. On a capped site, old revisions are automatically deleted as new ones arrive, so a burst of bad writes can push the last good version out. Revisions are reachable over REST too, and deleting one is permanent: the reference says the force argument is required to be true, as revisions do not support trashing.

On concurrency, plan for the API to give you no help. Treat every post update as last-write-wins and make your own process supply whatever ordering you need, for example one writer per post at a time.

How this lines up with what we saw

Every part of the incident described above fits inside documented behavior. The content field replaced the body because that is what an update does. The 200 came back because the request was valid. And the rendered-versus-raw trap is the gap between the view and edit contexts.

The verification that settled our case was also built from documented parts: a revision count that moved from 2 to 3, a modification timestamp that advanced, and a byte length read back from storage and compared with the expected body. None of those came from the response to the write.

A practical checklist for writes through the WordPress REST API

  1. Decide first whether the request sends content. If it only sends scalar fields like title, slug or status, keep the process light.
  2. If it does send content, read the post with context=edit and save the raw body somewhere outside the process doing the write.
  3. Build the new body from raw source, never from content.rendered.
  4. Check the site’s revision limit before a batch. A low cap shortens how far back you can go.
  5. Run one writer per post. Do not rely on the API to order competing writes; serialize them yourself.
  6. After the write, fetch only the field you changed with _fields, from a separate process, and compare length, start, end and structure against what you meant to store.
  7. If the read-back is wrong, restore from the saved copy before you investigate.

More questions

Does WordPress keep a revision every time the REST API updates a post?

Revisions live in the posts table, but how many survive depends on the site. WP_POST_REVISIONS and the wp_revisions_to_keep filter can limit them or turn them off, so check the setting on the site you are writing to instead of assuming a revision exists.

Why does my GET request not return content.raw?

The default context is view, which returns the rendered field. Raw content comes back with context=edit, because the raw field only belongs to the edit context.

Grounded in: our own batch incident on 2026-08-14, when a retry agent wrote its escaping test string into the live content field of a published article, and the controlled comparison run on 2026-08-15. Verified: the expected stored body reconstructed locally from the saved original measured 12,282 bytes and the live read-back measured 12,282 bytes; the modification timestamp advanced and the revision count moved 2 to 3. No green tick was consulted. The runnable procedure lives in the member library.

Related guides

Skill Spotlights · Published

Agent Skills: The Four Bytes Four Gates Missed

Four inspection gates checked 22 agent skills but missed four incorrect bytes. This case shows why teams need different verification methods, not simply more checks of the same kind.

Skill Spotlights · Published

What the JetEngine REST API actually reaches

JetEngine's REST API exposes two routes with different authentication behavior. Learn what the live endpoint tests proved, and where reads and writes stop.

Skill Spotlights · Published

The Slide Deck Check That Read Four-Pixel Text

A slide deck passed its checks despite four-pixel text. Learn to measure rendered legibility at delivery resolution and verify citations against their exact source spans.