Technote

Development workflow Practical

Series The REST API and going headless Part 3 of 8

Your own endpoint: namespace, permission callback, schema

permission_callback is not optional. Omit it and the route is public, with a core warning to match. An args schema then keeps validation out of your handler entirely.

Eventually core routes stop fitting. You want several resources merged into one response, or you need to express a verb from your own domain — cancel an order, start a scan. That is what register_rest_route() is for, and it must be called inside rest_api_init: any earlier and there is no REST server yet.

The namespace carries a version

Write namespaces as vendor/version — wper/v1. The version is what lets you open a v2 alongside it the day the response shape has to change, and migrate consumers without breaking them. An unversioned namespace is regretted at the first change.

The vendor half has to be unique to your project. If two plugins register into the same namespace, the later registration is silently ignored.

add_action( 'rest_api_init', 'wper_register_routes' );

function wper_register_routes() {
	register_rest_route(
		'wper/v1',
		'/orders/(?P<id>d+)',
		[
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => 'wper_get_order',

			// Required. Omit it and the route is public, with a core warning.
			'permission_callback' => function () {
				return current_user_can( 'edit_posts' );
			},

			'args'                => [
				'id'     => [
					'type'              => 'integer',
					'required'          => true,
					'sanitize_callback' => 'absint',
				],
				'detail' => [
					'type'    => 'string',
					'enum'    => [ 'summary', 'full' ],
					'default' => 'summary',
				],
			],
		]
	);
}

permission_callback is not an optional argument

Since WordPress 5.5, a route registered without permission_callback triggers a _doing_it_wrong() notice. The notice reads politely enough to mislead, so be clear about this: the warning does not close the route. It carries on serving, publicly. And with debugging off in production you will not even see the warning.

Hence the convention: when a route really is meant to be public, write '__return_true' explicitly. It lets the next reader tell “deliberately open” from “forgotten”. Webhook receivers are exactly that case, and they get their own part later in this series.

The callback may return true, false or a WP_Error. Return false and core picks the status for you — 401 when nobody is logged in, 403 when someone is but lacks the capability. Return a WP_Error when you want to choose.

The schema validates, and it runs before the permission callback

Declare types, required flags, enum, default and sanitize_callback in args and core validates and sanitises the request before it ever reaches your handler. No stack of if ( ! isset( … ) ) guards, and your failure responses match the shape core routes already produce.

Dispatch order — argument validation happens before the permission callback

Knowing the order is useful. Because validation comes first, the permission callback receives an already-sanitised request — it is safe to take $request['id'] at face value and look up ownership with it. The flip side is that a malformed argument returns 400 before permissions are ever consulted, which can look confusing when you expected a 403.

Handlers return a WP_REST_Response or a WP_Error. A plain array works too, but passing it through rest_ensure_response() leaves you somewhere to attach headers later.

function wper_get_order( WP_REST_Request $request ) {
	$order = get_post( $request['id'] );

	if ( ! $order || 'wper_order' !== $order->post_type ) {
		return new WP_Error( 'wper_order_not_found', 'Order not found.', [ 'status' => 404 ] );
	}

	return rest_ensure_response( [
		'id'     => $order->ID,
		'status' => $order->post_status,
	] );
}

The procedure we follow when adding a layer like this to somebody else’s site is written out on our process page, and the wider design background sits in the development workflow archive.

Next part

With a route in place, you have to decide who may call it. Next: authentication — what cookies and nonces, application passwords and tokens are each for, and how their threat models differ.

More on this topic

All technotes

Development workflow Advanced

The design QA checklist to run before release

Most design problems found after release are catchable before it, in order. Here is the pass, in three stages: rules, states and real devices.

Designers 6 min read

Development workflow Advanced

Never deploy without a way back

A rollback is not one button. It is two tracks — code and database — with an order to reverse them in, written down before you deploy.

Designers 9 min read

₩270,000 · Join the program