register_rest_route()

Add your own REST route: namespace, route regex, method constants, argument validation and the required permission_callback.

Syntaxregister_rest_route( string $route_namespace, string $route, array $args = array(), bool $override = false ): bool

register_rest_route() adds a route to the REST server. It was introduced in WordPress 4.4 and must be called on or after the rest_api_init hook — since WordPress 5.1, calling it earlier triggers a _doing_it_wrong() notice.

Namespace and route

The first argument is your namespace, conventionally vendor/v1, with no leading or trailing slashes. The second is the route, and it is a regular expression, not a path. Named groups become URL parameters: /book/(?P<id>\d+) exposes $request['id'] and refuses anything non-numeric before your callback runs.

Methods: shorthand and longhand

The methods key takes a comma-separated string, an array, or one of the WP_REST_Server constants. These pairs are the same value:

ConstantString it equals
WP_REST_Server::READABLE'GET'
WP_REST_Server::CREATABLE'POST'
WP_REST_Server::EDITABLE'POST, PUT, PATCH'
WP_REST_Server::DELETABLE'DELETE'
WP_REST_Server::ALLMETHODS'GET, POST, PUT, PATCH, DELETE'

Pass one options array for a single handler, or an array of arrays to give the same route a different callback per method.

args and permission_callback

Each entry in args declares one parameter with required, type, default, enum, validate_callback and sanitize_callback. All of it runs before your callback, so invalid input never reaches your code. permission_callback has been required since WordPress 5.5; omitting it logs a _doing_it_wrong() notice.

Example

Example · php
<?php
/**
 * A read/write route for a single book, at
 *   /wp-json/acme/v1/books/<id>
 */
add_action( 'rest_api_init', 'acme_register_book_routes' );

function acme_register_book_routes() {
    register_rest_route(
        'acme/v1',                    // namespace: vendor/version, no slashes
        '/books/(?P<id>\d+)',         // regex; the named group becomes a param
        array(
            // Shorthand: 'GET'. Longhand: WP_REST_Server::READABLE.
            // They are the same string; the constant documents intent.
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => 'acme_get_book',
            'permission_callback' => '__return_true', // public read, deliberate
            'args'                => array(
                'id' => array(
                    'description'       => __( 'Book post ID.', 'acme' ),
                    'type'              => 'integer',
                    'required'          => true,
                    'sanitize_callback' => 'absint',
                    'validate_callback' => function ( $value, $request, $key ) {
                        return 'book' === get_post_type( (int) $value );
                    },
                ),
            ),
        )
    );
}

function acme_get_book( WP_REST_Request $request ) {
    // Validation already guaranteed this is a real book ID.
    $post = get_post( (int) $request['id'] );

    return rest_ensure_response(
        array(
            'id'    => $post->ID,
            'title' => get_the_title( $post ),
            'isbn'  => get_post_meta( $post->ID, '_acme_isbn', true ),
        )
    );
}

When to use it

  • Exposing a plugin's data to the block editor without writing an admin-ajax handler.
  • Building a webhook receiver that a payment provider POSTs to.
  • Giving a single-page app a purpose-built endpoint instead of five wp/v2 calls.
  • Versioning a public integration by registering acme/v2 beside acme/v1.

More examples

One route, a different callback per method

The nested form is how core registers its own collections: reading a list and creating an item are the same URL with different permissions and different arguments. Keeping them in one call means the route, and the 'schema' entry describing it, are declared in one place.

Example · php
<?php
add_action(
    'rest_api_init',
    function () {
        // Passing an array OF arrays registers several endpoints on one route.
        register_rest_route(
            'acme/v1',
            '/subscribers',
            array(
                array(
                    'methods'             => WP_REST_Server::READABLE, // 'GET'
                    'callback'            => 'acme_list_subscribers',
                    'permission_callback' => function () {
                        return current_user_can( 'list_users' );
                    },
                ),
                array(
                    'methods'             => WP_REST_Server::CREATABLE, // 'POST'
                    'callback'            => 'acme_create_subscriber',
                    'permission_callback' => '__return_true', // public sign-up
                    'args'                => array(
                        'email' => array(
                            'type'              => 'string',
                            'format'            => 'email',
                            'required'          => true,
                            'sanitize_callback' => 'sanitize_email',
                        ),
                    ),
                ),
                // A shared schema callback describes the resource for /wp-json
                // and for the OPTIONS request the block editor sends.
                'schema' => 'acme_subscriber_schema',
            )
        );
    }
);

Constraining a query parameter with enum and default

Declaring constraints in args rather than checking them in the callback gets you three things for free: a 400 with a useful message, the parameter documented in the route's schema at /wp-json/acme/v1, and a callback that can trust its input. Reach for it on any endpoint with more than one optional parameter.

Example · php
<?php
register_rest_route(
    'acme/v1',
    '/reports',
    array(
        'methods'             => 'GET',
        'callback'            => 'acme_get_reports',
        'permission_callback' => function () {
            return current_user_can( 'edit_posts' );
        },
        'args'                => array(
            'range'    => array(
                'type'    => 'string',
                // A request with range=decade is rejected with 400
                // rest_invalid_param before acme_get_reports() is called.
                'enum'    => array( 'day', 'week', 'month' ),
                'default' => 'week',
            ),
            'per_page' => array(
                'type'              => 'integer',
                'default'           => 10,
                'minimum'           => 1,
                'maximum'           => 100,
                // Since WP 4.7 the built-in validator enforces minimum/maximum
                // from the schema; this only coerces the type.
                'sanitize_callback' => 'absint',
            ),
        ),
    )
);

Discussion

  • Be the first to comment on this lesson.