WordPress has the REST API switched on from the moment it is installed. No plugin, no setting. Open /wp-json/ in a browser and you get every namespace and route this site publishes, as JSON. It is worth knowing even if you never plan to go headless, because this is the path the block editor uses to save your posts.
What is already open
The main namespace core registers is wp/v2: posts, pages, media, categories, tags, users, settings and search. Custom post types only join the list if they were registered to — which is the subject of the next part.
GET /wp-json/ namespaces and route index
GET /wp-json/wp/v2/posts published posts (no auth needed)
GET /wp-json/wp/v2/posts/12 a single post
OPTIONS /wp-json/wp/v2/posts the schema and allowed methods
That last line earns its keep. Send OPTIONS to a route and you get back a schema listing every argument, its type, whether it is required, and its default. It is quicker than hunting for documentation and, more usefully, it describes what is actually registered on this install — including routes a plugin added.
The boundary between reading and writing is sharp. Reading published content needs no authentication; almost everything else does.
The shape of a response — context, _fields, _embed
The same resource returns different fields depending on context. The default view gives public fields, embed gives a short summary, and edit adds the raw values — but edit requires the capability to edit.
If a listing only needs titles and links, ask for _fields=id,title,link. The payload shrinks and the server stops building fields nobody reads. Going the other way, _embed pulls the featured image, author and terms into _embedded so you make fewer round trips. They optimise in opposite directions, so decide what a given request is trying to save before choosing.
Pagination and errors
Collections return ten items by default. per_page adjusts that, with a hard ceiling of 100. The total count does not come in the body — it comes in response headers.
X-WP-Total: 137
X-WP-TotalPages: 14
Clients that ignore those headers and loop “until a page comes back empty” are common. Requesting a page past the last one returns a 400 error, not an empty array, so that strategy generates one error on every single traversal.
Errors arrive as an HTTP status code and a JSON body, and the body always has the same shape.
{"code":"rest_post_invalid_id","message":"Invalid post ID.","data":{"status":404}}
code is the stable string your client branches on; message is a sentence for a human. Never branch on message — change the site locale and the sentence changes. For the same reason, routes you write should treat their own code values as a public contract.
Wider development habits around building on the API live in the development workflow archive, and how we approach touching these layers on somebody else’s installation is published on our process page.
Next part
That is core covered. Next: getting your own post types and meta into that same list — what show_in_rest actually does, and why one line is not enough when it comes to meta.