If your custom post type is missing from /wp-json/wp/v2/, there is one reason: show_in_rest was never set. That single line does more than create a route — it is also the switch that turns on the block editor, because the editor runs on REST. Leave it false and editing falls back to the classic editor.
The post type — one line, plus what comes with it
register_post_type(
'wper_tool',
[
'public' => true,
'show_in_rest' => true, // route + block editor
'rest_base' => 'tools', // otherwise the route uses the raw post type key
'supports' => [ 'title', 'editor', 'custom-fields' ],
]
);
Without rest_base the address becomes /wp-json/wp/v2/wper_tool. That works, but it publishes your internal prefix as part of a public API, and the day you change the prefix every client breaks with it. Decide the public name separately, from the start.
Taxonomies follow the same pattern: give register_taxonomy() a show_in_rest and a rest_base, and you get term routes plus that taxonomy as a field on post responses.
The last line of that example matters more than it looks. If the post type does not support custom-fields, the response schema never gets a meta property at all. You can register meta perfectly and see nothing, and because the cause is not in the meta registration you will look in the wrong place for a while.
Meta takes more than one line
To put meta on the API you must register it with register_post_meta(). An unregistered key never appears, however much data sits in it. Registration also needs a type, because REST builds a schema first and validates values against it — with no type there is nowhere to put the value.
register_post_meta(
'wper_tool',
'github_url',
[
'type' => 'string',
'single' => true, // omit this and REST returns an array
'show_in_rest' => true,
'auth_callback' => function ( $allowed, $meta_key, $post_id ) {
return current_user_can( 'edit_post', $post_id );
},
]
);
Forgetting single is the most common slip. It defaults to false, so the field comes back as "github_url": ["https://…"] — an array where the client expected a string — and the client quietly renders nothing.
For arrays and objects, pass [ 'schema' => [ … ] ] instead of true so the item shape is declared. Registration refuses a complex type with no schema, which is the right default: it declines to publish a value the API has no way to validate.
Reading and writing use different doors
This is where accidents happen. auth_callback gates writing. Reading is not affected by it at all — anyone who can read the post gets the meta with it.
Protected keys beginning with an underscore default to write denied when no auth_callback is given, which produces the symptom “the value saves but the editor cannot change it”. Conversely, the moment you set show_in_rest that value becomes publicly readable, so do not put internal data on the API merely because you would like to edit it in the block editor.
The test is simple: would you be comfortable if this value were printed in the page source? If not, handle it with a meta box and admin_post instead. Payment credentials and server access details stay well away from the API for exactly this reason.
How we decide on structured field design is covered further in the Themes & plugins archive, and the procedure for rolling a data layer like this onto a live site is on our process page.
Next part
Sooner or later core routes are not enough. Next: your own endpoints — choosing a namespace, why permission_callback is a required argument, and how an args schema saves you from hand-writing validation.