Creating an add-on page with the REST API: Part 1

Screenshot of the Members plugin add-ons admin screen/page

When I released version 2.0.0 of the Members plugin a few months ago, I created a new admin screen that listed all of the plugin’s add-ons. I used the WordPress REST API on this site and some custom code to get that data within the Members plugin. Basically, this is a way for me to directly communicate with my plugin users.

I even cleared this project with the plugin team ahead of time to make sure I wasn’t doing anything out of bounds in terms of the guidelines.

Most tutorials you’ll read on the REST API talk extensively about all the cool JavaScript stuff you can do. They go into stuff like ES6, Webpack, Babel, and so on. If you’re coming from a PHP development background, much of this can be confusing, especially if you simply want to know what sort of things you can do with the REST API.

In this tutorial, I’ll only be sharing PHP code, and our use of the REST API will only be for reading/getting data. This is the type of foundational stuff that should be helpful to those who just want to learn some of the basics.

In part 1 of this tutorial, we’re going to cover the REST API aspects that will run on our own sites. In part 2, we’ll build an admin screen to show off our add-ons to plugin users.

If building an add-on screen for your plugin isn’t your thing, you may still find this tutorial helpful. What we’re really doing is learning how to retrieve data from our site using the REST API. There are numerous applications for this.


I’m going to make some assumptions in this tutorial. Primarily, I’m going to assume you have some development experience under your belt. If you haven’t built a few plugins, this is probably not the best starting point. A general working knowledge of WP concepts such as the following are necessary:

  • Some idea of what the REST API is.
  • Sanitizing and validating data.
  • Querying and looping through posts.
  • Transients.
  • Creating custom admin screens.
  • A way to manage your “plugin” posts on your site.

I highly recommend Plugin Developer for managing your plugin portfolio. It’s a plugin I built for people who build plugins, and I personally use it here on Theme Hybrid. Use whatever you want. Just note that I’m using a custom post type of “plugin” and some custom functions from Plugin Developer in some of the code examples.

Building with the API

Now, we’re getting into the meat of things. We need a way for our site to share data. There are many ways to actually do this, but the REST API makes this simple by creating some standard ways of handling the more complex bits.

Creating a REST route

The first step is creating a custom REST route. A route is basically the part of the URL that tells us what to create, read, update, or delete (CRUD). If we have a URL of, the th/v1/plugins is the “route”. The th/v1 is the namespace and version for the route.

And, that’s exactly what we’re doing in this next example code. We’re registering a custom route with WordPress.

I’m using the th/v1 namespace. th is just short for “Theme Hybrid”. You should change that to something unique to your project. Namespaces require both the name and version. Note: Versioning is awesome because you have a back-compatible way to handle breaking changes in the future.

add_action( 'rest_api_init', 'thapi_register_routes' );

function thapi_register_routes() {

    $namespace = 'th/v1';
    $route     = 'plugins';

            'methods'  => \WP_REST_Server::READABLE,
            'callback' => 'thapi_get_plugins',
            'args'     => array(
                'addons' => array(
                    'description'        => 'Limit results to specific parent plugin.',
                    'type'               => 'string',
                    'sanitize_callback'  => 'sanitize_text_field',
                    'validate_callback'  => 'thapi_validate_addon_option',
                    'enum'               => thapi_get_addon_options()

We used the register_rest_route function to register our custom route. The function takes in four parameters:

register_rest_route( $namespace, $route, $args = array(), $override = false );

The third parameter of $args is an array of options for the endpoint.


The methods argument is how we communicate over HTTP. Our only concern is with “reading” or “getting” data, so we set this value to \WP_REST_Server::READABLE.

You can also create, update, and delete data, but those methods are outside the scope of this tutorial.


This is the endpoint callback function that’s executed when trying to read data. The callback should only ever get and return data that already exists. It shouldn’t do anything else.


Yes, the $args array has an args parameter — awesome job on the naming there, WP devs.

This is an array of arrays for additional parameters on our URL. Because we’re looking to find add-ons for our plugin, I’m creating an addons parameter. In the case of the Members plugin, the URL to access its add-ons would be

Our custom parameter is addons. And, we’re letting WP know how we’re going to handle that scenario. Here’s another look at that bit of code:

'addons' => array(
    'description'        => 'Limit results to specific parent plugin.',
    'type'               => 'string',
    'sanitize_callback'  => 'sanitize_text_field',
    'validate_callback'  => 'thapi_validate_addon_option',
    'enum'               => thapi_get_addon_options()
  • description – Just a basic text description of the parameter.
  • type – The type of data the value should be. In our case, we’re looking for the plugin slug, which is a string.
  • sanitize_callback – This is the function used to sanitize the value to make sure it’s safe.
  • validate_callback – I’m using a validate callback to whitelist only a specific set of allowed values.
  • enum – This is the array of allowed values.

Allowed values

As mentioned above, I’m only going to ever allow certain values for my addons parameter. So, I created a custom function to return this array of values:

function thapi_get_addon_options() {

    return array( 'members' );

For my purposes, I simply hard-coded this list. In a real project for others to use, I’d create a database option to store these values. Since I’m the only one touching the code, I kept it simple.

Additionally, you could also use post IDs (members is the post slug). I prefer working with slugs just so that I can easily remember things.

Validating the add-on value

When validating, we need to do two things:

  1. Figure out if the value is the right type of data.
  2. Figure out if the data is one of our values.

If one of those is not true, we return a WP_Error. If both are true, we simply return true to let WP know that the value is valid.

Here’s a look at the validation callback:

function thapi_validate_addon_option( $value, $request, $param ) {

    if ( ! is_string( $value ) )
        return new WP_Error( 'rest_invalid_param', 'The addons argument must be a string.', array( 'status' => 400 ) );

    // Get the request attributes.
    $attributes = $request->get_attributes();

    // Get the arguments for the 'addons' parameter.
    $args = $attributes['args'][ $param ];

    // If not in the array of valid addons, return an error.
    if ( ! in_array( $value, $args['enum'], true ) )
        return new WP_Error( 'rest_invalid_param', sprintf( '%s is not a valid value for the addon parameter.', $value ), array( 'status' => 400 ) );

    return true;

Sanitizing the add-on value

Because this is a basic text field, I simply used sanitize_text_field. If you’re writing something more complex, you may need to build a custom callback function for this. Just be sure to make sure the data is safe to use.

The endpoint callback

This is where the magic happens. We’ve done a lot of work. Now, we actually want to return some add-ons for our plugin.

This is the callback function for actually getting and returning our add-on plugins.

For this, you primarily need to know how to use a WP_Query call and loop through your posts. I’m not going to walk you through every line of code below. Any plugin author who is diving into the REST API should be able to grasp what’s going on (feel free to ask questions in the comments if necessary).

A few points to note:

  • I’m using transients to temporarily store data. Because this data isn’t going to change often, I’m storing it for 1 day.
  • I’m handling both getting all plugins from the th/v1/plugins route and the ?addons=xxx parameter. You could split this up into multiple functions if you have many parameters.
  • All of the pdev_*() function calls are from my Plugin Developer plugin. They should be self-explanatory and can be replaced with custom or WP functions as need be.
  • The function must return data, not output it to the screen. A simple array of arrays will work fine here. This will all get turned into JSON in the end.
function thapi_get_plugins( $request ) {

    $data = array();

    // Initial query args.
    $query_args = array(
        'post_type'      => 'plugin',
        'posts_per_page' => -1

    // Check if this is a request for addons of a specific plugin.
    if ( ! empty( $request['addons'] ) ) {

        // If we have the data cached, go ahead and return it.
        if ( $addon_data = get_transient( "th_api_addons_{$request['addons']}" ) ) {

            return $addon_data;

        // Find the parent plugin to see if it exists.
        $parent = get_page_by_title( sanitize_title( $request['addons'] ), 'OBJECT', 'plugin' );

        // If we have a parent plugin, add it as the `MB_MARKDOWN_HASH8d170c03c835f32f6ac41e4e9c989bdfMB_MARKDOWN_HASH` parameter.
        if ( is_object( $parent ) )
            $query_args['post_parent__in'] = array( $parent->ID );

    // Generate a new WP Query.
    $plugins = new \WP_Query( $query_args );

    // If we find some plugins, let's roll.
    if ( $plugins->have_posts() ) {

        while ( $plugins->have_posts() ) {


            // If the plugin is for purchase, we're just going to return the primary
            // plugin page for the time being.
            $purchase_url = get_post_meta( pdev_get_plugin_id(), 'edd_download_id', true ) ? pdev_get_plugin_url() : '';

            // Get the author's URL set in their profile.
            $author_url = get_the_author_meta( 'url', pdev_get_plugin_author_id() );

            // Sets up all the data that we want to return for the plugin.
            $data[] = array(

                'title'   => pdev_get_plugin_title(),
                'url'     => pdev_get_plugin_url(),
                'slug'    => get_post()->post_name,
                'excerpt' => pdev_get_plugin_excerpt(),

                'author' => array(
                    'name' => pdev_get_plugin_author(),
                    'url'  => $author_url ? $author_url : pdev_get_plugin_author_url()

                'meta' => array(
                    'purchase_url'  => $purchase_url,
                    'download_url'  => pdev_get_plugin_download_url(),
                    'rating'        => pdev_get_plugin_rating(),
                    'rating_count'  => pdev_get_plugin_rating_count(),
                    'install_count' => pdev_get_plugin_install_count()

                'media' => array(
                    'icon'   => array( 'url' => pdev_get_plugin_icon_url( pdev_get_plugin_id(), array( 128, 128 ) ) )

        // If serving up data for an addon plugin, store it as a transient.
        if ( $data && ! empty( $request['addons'] ) ) {

            set_transient( "th_api_addons_{$request['addons']}", $data, DAY_IN_SECONDS );

    return $data ? rest_ensure_response( $data ) : rest_ensure_response( null );

You can see the actual JSON output of this here: Members Add-ons (note: a JSON reader browser extension is helpful for nicely formatting the output).

Coming in part 2

We’ve covered a lot of ground in a reasonably short tutorial. There’s so much more that’s possible with the REST API, but this should give you a good start on the basics.

In part 2, we’ll build an admin screen for actually using that JSON data that’s returned from a request.