Building a custom WordPress REST API endpoint

I got a call from a client running a busy WooCommerce shop. Their inventory was a mess. They use a third-party warehouse system to manage stock, but the only way to update the site was to export a CSV by hand and import it, every single day. It was slow and error prone. They needed the warehouse system to tell the website when stock changed on its own, and the way to do that is a custom WordPress REST API endpoint.

This is a classic problem. You need to let an external service talk to WordPress, but you need to do it securely and efficiently. You need a private doorbell, not a wide-open door.

Don’t hack it, build it right

My first thought, years ago, might have been to create a standalone PHP file in the theme. The warehouse could POST to my-site.com/wp-content/themes/my-theme/update-stock.php. And yeah, it would work, but it is a terrible idea. You are completely outside the WordPress environment, so you have to handle your own security, your own database connection, everything. It is a security hole waiting to be exploited.

The WordPress REST API is the answer. But you do not need the whole WP_REST_Controller class structure for something this simple. That is for building a suite of endpoints for a resource like posts or users. We only need one endpoint, and that is where register_rest_route comes in.

Your first custom WordPress REST API endpoint

We add our own endpoint by hooking into rest_api_init. The important part is the permission_callback. This is our bouncer. It checks for a secret key and makes sure the request is legit before it ever touches our logic. If this callback does not return true, WordPress kills the request.

add_action( 'rest_api_init', function () {
  register_rest_route( 'myplugin/v1', '/update-stock/', array(
    'methods'  => 'POST',
    'callback' => 'my_stock_update_callback',
    'permission_callback' => function ( $request ) {
      // Simple secret key authentication
      $secret_key = 'your-super-secret-key-here';
      $provided_key = $request->get_header( 'X-Secret-Key' );
      return $provided_key === $secret_key;
    },
  ) );
} );

function my_stock_update_callback( $request ) {
  $sku = sanitize_text_field( $request->get_param( 'sku' ) );
  $stock = (int) $request->get_param( 'stock' );

  if ( empty( $sku ) ) {
    return new WP_Error( 'no_sku', 'SKU not provided.', array( 'status' => 400 ) );
  }

  $product_id = wc_get_product_id_by_sku( $sku );

  if ( $product_id ) {
    $product = wc_get_product( $product_id );
    $product->set_stock_quantity( $stock );
    $product->save();
    return new WP_REST_Response( array( 'status' => 'success', 'new_stock' => $stock ), 200 );
  }

  return new WP_Error( 'invalid_sku', 'Product not found for SKU.', array( 'status' => 404 ) );
}

What’s the big deal?

This approach is clean and secure, and it uses the WordPress core systems. We are not reinventing the wheel. We let WordPress handle the routing and security, and we just provide the specific logic. Building small, focused systems is something I have learned over years in the trenches, and Carl Alexander explains the idea on his blog if you want to go deeper into the theory.

This stuff gets complicated fast. If you are tired of debugging someone else’s mess and just want your site to work, drop my team a line. We have probably seen it before.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.