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/versionwper/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 Practical

Turning taste arguments into rule checks

"It feels a bit cramped" can be neither argued with nor fixed. Spacing off the scale, colour off the palette, contrast below threshold, missing states — four rules…

Designers 9 min read

Development workflow Practical

Do not swap everything at once

A full swap makes every problem appear at the same moment — which means none of them can be attributed. So you switch one template at a time.

Designers 6 min read

Development workflow Practical

Adding an SCSS build, and whether to commit the output

WordPress themes are expected to deploy without a build step, which leads to the opposite conclusion from ordinary application code — and to its own costs.

Developers 7 min read

₩270,000 · Join the program