WordPress REST API Custom Endpoint

The built in routes cover posts, pages and more, but often you need your own: send a contact request, return a special list, or run a calculation. With register_rest_route() you can add an endpoint in a few lines. Do it in a plugin.

A simple GET endpoint

<?php
/**
 * Plugin Name: TC API Demo
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

add_action( 'rest_api_init', function () {

	// GET /wp-json/tc/v1/hello?name=Riya
	register_rest_route( 'tc/v1', '/hello', array(
		'methods'             => 'GET',
		'callback'            => 'tc_api_hello',
		'permission_callback' => '__return_true',   // public endpoint
		'args'                => array(
			'name' => array(
				'default'           => 'friend',
				'sanitize_callback' => 'sanitize_text_field',
			),
		),
	) );

	// GET /wp-json/tc/v1/square/5  (the number must be digits)
	register_rest_route( 'tc/v1', '/square/(?P<n>\d+)', array(
		'methods'             => 'GET',
		'callback'            => 'tc_api_square',
		'permission_callback' => '__return_true',
		'args'                => array(
			'n' => array(
				'validate_callback' => function ( $value ) {
					return is_numeric( $value ) && $value <= 1000;
				},
			),
		),
	) );

	// POST /wp-json/tc/v1/notes  (only for users who can edit posts)
	register_rest_route( 'tc/v1', '/notes', array(
		'methods'             => 'POST',
		'callback'            => 'tc_api_add_note',
		'permission_callback' => function () {
			return current_user_can( 'edit_posts' );
		},
		'args'                => array(
			'text' => array(
				'required'          => true,
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_text_field',
			),
		),
	) );
} );

function tc_api_hello( WP_REST_Request $request ) {
	return array( 'message' => 'Hello, ' . $request['name'] . '!' );
}

function tc_api_square( WP_REST_Request $request ) {
	$n = (int) $request['n'];
	return rest_ensure_response( array( 'number' => $n, 'square' => $n * $n ) );
}

function tc_api_add_note( WP_REST_Request $request ) {
	$id = wp_insert_post( array(
		'post_type'   => 'post',
		'post_title'  => $request['text'],
		'post_status' => 'draft',
	) );
	if ( is_wp_error( $id ) ) {
		return new WP_Error( 'tc_save_failed', 'Could not save the note.', array( 'status' => 500 ) );
	}
	return new WP_REST_Response( array( 'id' => $id, 'text' => $request['text'] ), 201 );
}

The parts of an endpoint

PartMeaning
Hook rest_api_initThe place where routes must be registered
Namespace tc/v1Your unique name and version. Use a name for your plugin, and change the version for big changes
RouteThe path. Use a regular expression for parts like (?P<n>\d+)
methodsGET, POST, PUT, DELETE (or constants like WP_REST_Server::READABLE)
callbackThe function which makes the answer
permission_callbackWho can use it. It is required. Use __return_true for a public endpoint
argsDescription of the parameters: default, required, type, validation and sanitizing

Test the endpoints

Terminal output

$ wp plugin activate tc-api
Plugin 'tc-api' activated.
Success: Activated 1 of 1 plugins.
$ curl -s "http://example.com/wp-json/tc/v1/hello" | jq -c .
{"message":"Hello, friend!"}
$ curl -s "http://example.com/wp-json/tc/v1/hello?name=Riya" | jq -c .
{"message":"Hello, Riya!"}
$ curl -s "http://example.com/wp-json/tc/v1/square/12" | jq -c .
{"number":12,"square":144}

Errors from validation and routes

Terminal output

$ curl -s "http://example.com/wp-json/tc/v1/square/5000" | jq -c '{code, message, status: .data.status}'
{"code":"rest_invalid_param","message":"Invalid parameter(s): n","status":400}
$ curl -s "http://example.com/wp-json/tc/v1/square/abc" | jq -c '{code, status: .data.status}'
{"code":"rest_no_route","status":404}

The number 5000 is a valid route, but it failed our validate_callback, so WordPress answered 400 with the code rest_invalid_param. The text abc does not match the route at all, so there is no route (404).

Protected endpoint

Terminal output

$ curl -s -X POST "http://example.com/wp-json/tc/v1/notes" -d "text=Call the plumber" | jq -c '{code, status: .data.status}'
{"code":"rest_forbidden","status":401}
$ curl -s -u "admin:APP_PASSWORD" -X POST "http://example.com/wp-json/tc/v1/notes" -d "text=Call the plumber" | jq -c .
{"id":5,"text":"Call the plumber"}
$ curl -s -u "admin:APP_PASSWORD" -X POST "http://example.com/wp-json/tc/v1/notes" | jq -c '{code, message}'
{"code":"rest_missing_callback_param","message":"Missing parameter(s): text"}

Without a login the permission callback said no (401). With the application password, the note was saved and the code is 201. When the required parameter text is missing, WordPress gives a clear error by itself.

Return errors in the right way

  • Return new WP_Error( 'code', 'Message', array( 'status' => 404 ) ) to send an error with the HTTP status.
  • Return an array or WP_REST_Response for a success.
  • Use rest_ensure_response() to wrap a value into a response object.
  • Do not echo or die() inside the callback. Return the data.
Note: Never forget the permission_callback. Since WordPress 5.5 a missing one gives a notice. For private data, check a capability, not only if the user is logged in. Next: REST API Post Comments.

Congratulations! Chapter Finished. Learn more about the similar topics:
Exercises & Assignments
No Content Found.
Interview Questions & Answers
No Content Found.