How to easily add a custom REST api to WordPress


In this post I’m going to go over the steps you need to go through to add a custom REST api to a WordPress site.

Why add a custom REST api

I find that one of the most frustrating aspects of working on a development version of a site is the requirement to populate it with content. Although it’s easy enough to create dummy content, it’s a time consuming activity to selectively import posts, custom post types, taxonomies and particularly media from a live site to a development site so that you can test code you are working on with realistic data.

For this reason I’m currently working on a plugin which allows me to sync content from the live versions of my sites to the development versions by just selecting the required posts, CPTs and Taxonomies and with a single action pull it in. This process uses a custom REST api running on the live site which reads data and transfers it to the development site where it’s added and / or replaces existing content.

So in this post I’m covering how to add the custom REST api, and I’m describing it in relation to the Plugin which I’m currently working on.

Adding a Custom REST api.

Many things in WordPress start by adding either a filter or an action and creating a REST api is no exception. The first thing necessary is to register the endpoint with wordpress and this is carried out during an action called ‘rest_api_init’. Before you get to that point however, you need to work out the namespace for your api.

The namespace is simply the last part of the URL you are going to use as the calling address and it distinguishes your REST api from any other REST apis your install of wordpress is set up to respond to.

To be clear, the REST api is going to appear at the address of the wordpress site, so if the site is at, the api will have an address which starts and that will be followed by /wp-json/. It is the part after that which is the namespace for your custom REST api.

My plugin is called SiteSync so I made the choice of site-sync/v1 as my namespace. The v1 is so that if at any point I need to change the data format I can just alter it to v2 and test for compatibility during any calls which are made. At the moment this isn’t strictly necessary because I’m the only one using this plugin, but if I decided to release it and allow others to use it, then the versioning would be important.

Registering the namespace

So the namespace I’ve adopted is /site-sync/v1. As I said above, the first part of creating the custom api is to register this namespace and that is done in the ‘rest_api_init’ action. The code below shows a sample class where this action is called and the first custom api route registered.

class SiteSync

public function __construct()
    add_action('rest_api_init', [$this, 'registerEndpoints']);

public function registerEndpoints()
    foreach (['list-taxonomy' => 'listTaxonomy'] as $route => $callback) {
              'methods' => 'GET',
              'callback' => [$this, $callback],
              'permission_callback' => [$this, 'allowDeny']

So the code above is creating the first endpoint called ‘list-taxonomy’ by registering that name in the site-sync/v1 namespace. After this is run, wordpress will be set up to accept a call to

and will route any request made to that url to the callback function named in the callback part of the request options – in this case a function called ‘listTaxonomy’ within the same class.

You may wonder why the registration is done in a foreach loop? Well that’s because there are actually several routes to set up and when the call is made in a loop I can just add the new routes without having to repeat a lot of code.

Api Permissions callback

As well as the callback method, there is also a permission_callback listed in the code and this callback is used to register a method which will decide if the caller has the correct authority to make the call. This method is not required – you can miss out that part of the registration if you want – but if you don’t supply a permission_callback anyone will be able to pull data from your site.

Now that may not matter – if you are just passing posts and taxonomies the data is probably in the public domain and it wouldn’t matter if anyone else found it, but for my application I wanted to add a bit of security in case I want to sync sensitive data at any point.

You will notice that the permission_callback is given the same name irrespective of which route and callback is set; it’s called ‘allowDeny’. This is so I only have to write one permission method and can use it for every path I set up in the api.

The permission callback is quite open to using sophisticated authorization techniques if you want, but for the purposes of my plugin I adopted a two pronged approach which checks that any request

  • has an identity header with a string id code which only the requesting site would know
  • provides a security hash which is constructed with a shared secret.
public function allowDeny(WP_REST_Request $request)
    if ($request) {
        $id = $request->get_header('x_ident');
        $validateHeader = $request->get_header('x_validate');
        if (!$id || !$validateHeader) {
          return false;
        $validate = explode('|', $validateHeader);
        $check = md5($validate[0] . $_ENV['secret']) == $validate[1];
        if ( ($id === SiteSyncOptions::getKey() && $check) ) {
          return true;
    return false;

This will be easier to follow in conjunction with the callLive function below.

The permission_callback is automatically passed the Request information in a WP_Request object by the WordPress REST api system. I make sure that all requests to the api are supplied with two http headers called ‘X-Ident’ and ‘X-Validate’.

The X-Ident header is set to the identity string which both ends of the request / response code understand. In the application I’m describing that’s the live server and the development machine. In my plugin this is set via a .env config file.

The X-Validate header is set up with two pieces of data joined with a ‘|’ pipe character. The first is simply a date string in a known format and the second is the md5 hash of the date string when it’s appended with a known secret which is known to the both ends of the communication. When the validation string is received it’s split on the ‘|’ and then the hash recalculated to make sure it matches the hash sent from the live system.

The purpose of this hash is to stop casual requests made to the api by any service probing for open apis. Since the whole request is run over https it won’t be easily viewed by outside parties so I think that it’s secure enough for my purposes. It would be possible however to add additional code to make the system more secure – for example a set of IP addresses could to be used to make sure the api will only respond to certain machines etc. It would also be easy in this case to only respond if the date sent was within a few seconds of the current time so a copied URL would not be valid etc.

The examples so far have shown how the REST api is set up – the next examples show how it is used to exchange data between the two machines and in the example code which follows rather than use the ‘list-taxonomies’ endpoint I’ve used ‘sync-taxonomies’ which also sends parameters as part of the call and is therefore more illustrative of the techniques used.

Sending and receiving data

The sending and receiving of data in my particular application consists of the following workflow:

  • The request for data is made by the Development machine which sends an https request to the api endpoint running on the live server. This is done with the Guzzle HTTP client in my case.
  • The request is received by the live machine and is first checked by the allowDeny method to make sure it is allowable. Assuming the headers have been set and are valid the request is routed to the callback function which was listed in the rest_api_registration call.
  • The callback receives the call and the list of parameters which were sent in the call.
  • After gathering the data the callback returns the data wrapped in a rest_response object
  • The development machine receives the data back as a Guzzle HTTP response.
  • Assuming the http status is 200, the client response is decoded from the json string held in the body and the data can then be used.

The first part of this is the call made to the api using the Guzzle http client.

In my particular application, this call is made via an Advanced Custom Fields form which, to take the simplest case of the taxonomies, has a select list of remote taxonomies and an action which can be performed – either replace or merge. When this action is selected the local development machine calls the live machine for the taxonomy data.

The code which makes the call to the live server looks like this:

public function doAction()
  $replacements = $this->callLive('sync-taxonomy', ['name' => $taxToSync]);
public function callLive($action, $params=[])
  $urlBase = getField('live_site_url');
  $key = getField('live_site_key');
  if ($urlBase && $key) {
    $client   = new Client();
    $date = date( "ihdmY" );
    $hash = md5($date.$_ENV['secret'] );
    $fullUrl = $urlBase.$action;
    if (!empty($params)) {
      $getParams = build_query($params);
      $fullUrl .= '?' . $getParams;
    try {
      $response = $client->request(
          'headers' => [
          'X-Ident' => $key,
          'X-Validate' => $date . '|' . $hash,
          'Accept' => 'application/json'
         ] );
        } catch (Exception $ex) {
	    return false;
        if ( $response->getStatusCode() == 200 ) {
          $body = (string) $response->getBody();
          $result = json_decode( $body );
          return $result;
        } else {
          return false;
    return false;

The $replacements is the list of taxonomies which will be returned from the live machine.

Some notes about this code:

The live site URL and the Identification key are held in Advanced Custom Fields form fields so they are fetched using the ACF get_field function, but in this code I have a helper method called getField which means I don’t need to pass the post_id parameter for each call.

The rest of the code is fairly self explanatory; The URL is constructed from the live site URL (i.e. the REST api endpoint) and the parameters and then a GET request is made with the Guzzle client. If it’s successful, the json data is read from the message body and converted to a PHP object and returned.

On the live machine the method which responds to this call looks like the code below.

public function syncTaxonomy( WP_REST_Request $request )
  $params = $request->get_params();
  if ( isset( $params['name'] ) && SiteSync::isLive() ) {
    $tax = get_taxonomies();
    if ( isset( $tax[ $params['name'] ] ) ) {
      $terms = get_terms(['taxonomy' => $params['name'], 'hide_empty' => false ]);
      $parents  = [];
      $children = [];
      foreach ( $terms as $term ) {
        if ( $term->parent == 0 ) {
          $parents[] = $term;
        } else {
          $children[] = $term;
      $return = [
        'parents'  => $parents,
        'children' => $children
      return rest_ensure_response( $return );
  return rest_ensure_response( 'Error: Name or action not set' );

This method receives the REST api call from the development machine and pulls the parameters from the WP_REST_Request object to find which taxonomy has been requested.

There is a check to make sure the code is running on the live site1 and then some fairly standard code to extract the taxonomy requested and split it into parents and children to make it easier to add the data on the development machine.

The return data is returned in a call to the inbuilt function rest_ensure_response which takes care of packaging the return data into a json data stream.

Useful tools when developing a REST api

One of the complexities of developing a custom REST api is debugging it during the phase when you are still sorting out the design. During this phase it’s useful to have a debugging tool which allows you to step through the code from an http call and see exactly what is happening.

During the debugging phase of my project I used PhpStorm, which has a great http client, but a lot of developers won’t necessarily have a licence for PhpStorm[efn_note]I happen to have a licence for PhpStorm because I use it for work[/efn_note] so it would be worth looking at the Postman http client. There is a free version available which would be a great help during that phase.

In order to run with proper data I installed my plugin on the live version of this site very early on in the project so that I could pull in some real data. Using the git deploy technique I’ve recently set up on my sites I found it pretty straightforward to make changes, debug with PhpStorm and then push to live and work on the development end of the project.

In conclusion I would find it useful to know if anyone would be interested in using the plugin I’m developing?

Although there are plugins which do similar things I couldn’t find anything which fitted my needs. Many of the plugins I found were designed to replicate a site exactly or move a site to a different host, but I wanted something which would allow me to select individual posts or other content and sync just that so I don’t end up with a bloated DB on development and could re-sync an individual post if necessary.

If this sounds something you would be interested in please let me know in the comments section below.

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

As you found this post useful...

Please consider sharing on social media!

  1. The slightly confusing aspect of working on this project is the fact that the same plugin runs on both live and development but only certain parts run at each end of the communication chain. To specify which portions of code should run I have an environmental variable which is set in a .env file which sets the plugin to run in either ‘live’ or ‘development mode’ []

Leave a Reply

Your email address will not be published. Required fields are marked *

Twig Template in Use Previous post How to easily use Twig templates in your WordPress plugin
Next post My miserable year at Kerry Ultrasonics in Hitchin