Context
By default, WooCommerce’s REST API search (/wp-json/wc/v3/products?search=...
) only searches product titles, descriptions, and excerpts. That means if you query by SKU, you’ll get an empty result set — even though the SKU is one of the most common identifiers developers use when syncing products, integrating with ERPs, or building external inventory tools.
This limitation frustrates many developers building integrations that rely on SKUs as a primary lookup key. While WooCommerce provides a dedicated ?sku=
filter for exact matches, it does not support partial SKU lookups or combine them with text-based searches.
The Limitation
The WooCommerce REST API delegates product queries to WordPress’s WP_Query
, which only performs full-text searches in post_title
, post_content
, and post_excerpt
.
SKUs are stored in post meta under the key _sku
, and meta fields are not included in the default ?search=
query for performance reasons.
As a result:
?search=ABC123
will not return a product with SKUABC123
.?sku=ABC123
will work, but only for exact SKU matches.- You cannot do partial SKU searches (e.g.,
?search=ABC
).
The Solution
To address this, we can extend the default WooCommerce REST search behavior without registering a new endpoint or modifying core files.
The plugin below intercepts the API response for the existing /wc/v3/products
route, searches for products whose _sku
meta field matches the term (exact or partial), and merges those products into the response that WooCommerce generates.
This means:
- You keep using
/wp-json/wc/v3/products?search=...
. - SKU matches automatically appear in the results.
- Partial matches are supported (or you can toggle to exact mode).
- No database schema changes or heavy SQL hacks are required.
How It Works
The plugin works in two phases:
- Detect when WooCommerce’s product endpoint is queried with a
?search=
parameter.
It uses therest_request_before_callbacks
hook to watch for requests on/wc/v3/products
. - After WooCommerce builds its normal response, the plugin queries the database for any products or variations whose
_sku
field matches the same term.
It then:- Converts those products into proper REST API objects using WooCommerce’s own controller.
- Merges them into the response data.
- Updates pagination headers (
X-WP-Total
andX-WP-TotalPages
) so the metadata remains consistent.
If a variation SKU matches, the parent product is included instead — ensuring consistency with how WooCommerce normally represents variable products.
The Code
Below is the full plugin code.
Create the folder wp-content/plugins/wc-rest-sku-augment
and save this file as wc-rest-sku-augment.php
.
Then activate it in WordPress Admin → Plugins.
<?php
/**
* Plugin Name: Woo REST SKU Search (Augment Default Search)
* Description: Augments /wp-json/wc/v3/products?search=... to include SKU matches (partial or exact) without new routes or core edits.
* Version: 1.0.0
* Author: The Hippoo team
* License: GPL-2.0+
*/
if (!defined('ABSPATH')) exit;
final class WCRestSkuAugment {
const MODE = 'partial'; // or 'exact'
private static $active = false;
private static $term = '';
private static $page = 1;
private static $per = 10;
public static function boot() {
add_filter('rest_request_before_callbacks', [__CLASS__, 'flag_request'], 10, 3);
add_filter('rest_post_dispatch', [__CLASS__, 'augment_response'], 10, 3);
}
private static function is_product_collection($request) {
$route = $request->get_route();
return is_string($route) && preg_match('#^/wc/v3/products/?$#', $route);
}
public static function flag_request($response, $handler, $request) {
self::$active = false;
if (!self::is_product_collection($request)) return $response;
$q = trim((string) $request->get_param('search'));
if ($q === '') return $response;
self::$active = true;
self::$term = $q;
self::$per = min(100, (int) ($request->get_param('per_page') ?: 10));
self::$page = max(1, (int) $request->get_param('page'));
return $response;
}
public static function augment_response($result, $server, $request) {
if (!self::$active || !self::is_product_collection($request) || !($result instanceof WP_REST_Response)) return $result;
$data = $result->get_data();
if (!is_array($data)) return $result;
$existing = array_column($data, 'id');
$sku_ids = self::find_by_sku(self::$term, self::MODE === 'partial');
if (!$sku_ids) return $result;
$parents = [];
foreach ($sku_ids as $id) {
$post = get_post($id);
if (!$post) continue;
$pid = ($post->post_type === 'product_variation') ? (int) $post->post_parent : (int) $post->ID;
if ($pid) $parents[$pid] = true;
}
$new_ids = array_values(array_diff(array_keys($parents), $existing));
if (!$new_ids) return $result;
$controller = new WC_REST_Products_Controller();
$remaining = max(0, self::$per - count($data));
$new_ids = array_slice($new_ids, 0, $remaining);
foreach ($new_ids as $pid) {
$p = wc_get_product($pid);
if (!$p) continue;
$prepared = $controller->prepare_object_for_response($p, $request);
if ($prepared instanceof WP_REST_Response) $data[] = $prepared->get_data();
}
$orig_total = (int) ($result->get_headers()['X-WP-Total'] ?? 0);
$new_total = $orig_total + count($new_ids);
$new_pages = ($new_total > 0) ? ceil($new_total / self::$per) : 1;
$result->set_data($data);
$result->header('X-WP-Total', (string) $new_total);
$result->header('X-WP-TotalPages', (string) $new_pages);
return $result;
}
private static function find_by_sku($term, $partial = true) {
global $wpdb;
if ($term === '') return [];
if ($partial) {
$like = '%' . $wpdb->esc_like($term) . '%';
$sql = $wpdb->prepare("
SELECT p.ID FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm
ON pm.post_id = p.ID AND pm.meta_key = '_sku'
WHERE p.post_type IN ('product','product_variation')
AND p.post_status IN ('publish','private')
AND pm.meta_value LIKE %s LIMIT 500
", $like);
} else {
$sql = $wpdb->prepare("
SELECT p.ID FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm
ON pm.post_id = p.ID AND pm.meta_key = '_sku'
WHERE p.post_type IN ('product','product_variation')
AND p.post_status IN ('publish','private')
AND pm.meta_value = %s LIMIT 500
", $term);
}
return array_map('intval', array_unique($wpdb->get_col($sql)));
}
}
add_action('rest_api_init', ['WCRestSkuAugment', 'boot']);
Example Usage
GET /wp-json/wc/v3/products?search=ABC123
→ Returns products whose name, description, or SKU contains “ABC123”.GET /wp-json/wc/v3/products?search=ABC
→ Returns partial matches (title/content + SKU).
To switch to exact SKU match only, edit this line:
const MODE = 'exact';
Limitations
- Performance: On very large catalogs (tens of thousands of SKUs), the extra meta query can add slight overhead. If you’re handling high-volume API traffic, consider caching results or using a dedicated SKU index.
- Pagination accuracy: The total count is approximated since SKU matches are merged after the main query runs.
- No deep filters: This plugin only affects the core
?search=
parameter, not other filters like?category=
or?attribute=
.