How to Create a Custom API endpoint in WordPress

silhouette of road signage during golden hour

There are many ways to create a custom API endpoint in WordPress. I personally like to build them against the base URL. If I was building one for this site it might be http://kevdees.com/kevdees-api/v1/.

I like to prefix my APIs to help prevent conflicts. If I was using a framework like Laravel or Slim I might take a different approach, but this is a post about WordPress.

To build an API in WordPress using your own URL is dead simple. You only need to use three hooks: query_vars, template_include.

We will be using PHP 7.4 and WordPress 5.8.

Register your API endpoint

For now, let us focus on establishing our endpoint. Later we can get into making use of it.

To register the endpoint against the base we can use add_rewrite_rule. This is the simplest way to get a custom URL going. We can use the admin_init hook to do this.

add_action('init', function() {
  $regex = 'kevdees-api/v1/([^/]*)/([^/]*)/?';
  $location ='index.php?_api_controller=$matches[1]&_api_action=$matches[2]';
  $priority = 'top';
  add_rewrite_rule( $regex, $location, $priority );
});

In this example, we are not following a pattern like REST or RPC. We are simply drawing out the basics so we can use these ideas to build any API we want.

Don’t forget to flush your rewrite rules.

The basics of add_rewrite_rule

If you are not familiar with add_rewrite_rule it is fine. We only need to know three things when building the example API: matching the URL, fetching our desired location, the priority of the endpoint.

Matching

Matching the URL in WordPress requires regular expressions. If you are not familiar with RegEx you need to be at this point (regex101.com is a great playground).

IMPORTANT: WordPress automatically escapes / for you and adds ^ to the beginning of your expression. I imagine they are trying to be developer-friendly and keep sloppy plugins from disturbing the user experience.

wp api regex example min
RegEx Example

Location

Now, every match we find is passed into $location as the $matches variable. This lets us assign them to specific query variables in the URL. In RegEx, matches are defined between parenthesis ().

In our code, we are looking for two matches 'kevdees-api/v1/([^/]*)/([^/]*)/?'. We then assign them to the location 'index.php?_api_controller=$matches[1]&_api_action=$matches[2]'.

api wordpress figure 1a v1
API Mapping Example

A URL request to http://kevdees.com/kevdees-api/v1/posts/update/ would fetch our location and assign the matches as directed http://kevdees.com/index.php?_api_controller=posts&_api_action=update.

Priority

Since WordPress has lots of rewrites already registered in the options table under the option_name of rewrite_rules we need to be sure ours is the first one it looks for. Otherwise, a page named kevdees-api might cause issues for us in the future.

We tell add_rewrite_rule to send our rewrite to the 'top' of the list and all is well.

$priority = 'top';
add_rewrite_rule( $regex, $location, $priority );

Establishing our custom matches as safe query variables

WordPress wants to keep you safe, believe it or not, and so we need to establish that our custom query variables are part of the program. In our case _api_controller and _api_action are the query variables in the $_GET request we want to make available.

Using the query_vars filter hook we can drop them into WordPress as safe. This will let us access them in our templates.

add_filter( 'query_vars', function($vars) {
    array_push($vars, '_api_controller');
    array_push($vars, '_api_action');
    return $vars;
} );

Plain and simple.

Constructing our Response

Finally, we are on to the fun part and really the thing we care about most. The Response.

To construct a response we need to override the default templating engine using the template_include filter hook. This will let us define what we want WordPress to send back.

We don’t want the themes index.php template file after all.

To detect a request coming to the API use get_query_var and check for our query variables. If they are present we should send a custom response.

add_filter( 'template_include', function($template) {
    $controller = get_query_var('_api_controller', null);
    $action = get_query_var('_api_action', null);

    if($controller && $action) {
        $template = __DIR__ . '/api/v1.php';
    }

    return $template;
}, 99 );

This example will produce the response using our v1.php file. We can respond with JSON, XML, or any other format we like. Just be sure to set the appropriate headers in the new template file.

JSON Response

If we wanted to send JSON we can use wp_send_json in our v1.php file.

wp_send_json(['api' => 'v1'] );

If we look at the HTTP response we will get exactly what we want.

HTTP/1.1 200 OK
Content-Length: 12
Content-Type: application/json; charset=UTF-8

{"api":"v1"}

Dare To Code

icon send thick Get the tips, links, and tricks on full-stack PHP development in your inbox with monthly emails from Kevin Dees.

Name(Required)
This field is for validation purposes and should be left unchanged.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.