How to Add Rego Lookup to a WooCommerce Store
Customers in an Australian parts store know their number plate. Far fewer know their exact model year, series or engine code. This guide adds a rego search to a WooCommerce store as a small WordPress plugin: the customer enters their plate and state, the store identifies the vehicle, asks which one when the plate could be more than one variant, and lists the products that fit. It is two files, about 210 lines in total, with no extra libraries.
How it works
- Each product stores the PlateAPI vehicle IDs it fits, in a custom field.
- The customer enters a plate and state in a form placed with a shortcode.
- Your WordPress server calls PlateAPI, so the API key never reaches the browser, and gets the vehicle with its
vehicle_id. - If the plate could be several variants, the customer picks theirs.
- The plugin lists the products tagged with that ID and remembers the vehicle in the browser for the next visit.
What you need
- WordPress with WooCommerce, PHP 7.4 or later, and pretty permalinks switched on (Settings, Permalinks: anything except "Plain").
- A PlateAPI key. The free tier (20 lookups a month, no card) is enough to build and test; for a live store, Lite is $29/month for 200 lookups and Starter $79/month for 600 (see pricing).
- Vehicle IDs for your products (step 1).
Step 1: tag products with vehicle IDs
The plugin reads a custom field called _plateapi_vehicle_ids: a comma-separated list of the PlateAPI vehicle IDs the product fits, for example 1380801,1346446,1330126. A vehicle ID identifies one variant across all of its model years, so a part that fits a 2015-2020 Hilux SR needs one ID, not six.
WooCommerce's built-in product CSV importer can fill the field for your whole catalogue. Export your products, add a column named Meta: _plateapi_vehicle_ids, and import the file with "Update existing products" ticked, so rows are matched to products by SKU. To work out the IDs from your existing fitment text ("Toyota Hilux 2015-2020 2.8L diesel"), walk the vehicle database once: the SKU linking guide has a short script that writes the IDs for each SKU to a CSV. The vehicle database is on Starter plans and above; plate lookups work on every plan.
For testing, tag any one product with 1380801. That is the vehicle the free sandbox plate TEST123 returns, so you can try the whole flow without using a lookup.
Step 2: add your API key
Put the key in wp-config.php, above the line that says "That's all, stop editing":
define('PLATEAPI_KEY', 'pk_live_YOUR_API_KEY');Step 3: the plugin
Create a folder wp-content/plugins/plateapi-rego/ with two files. The first registers two REST routes on your own site (one for the lookup, one for the products that fit a vehicle) and the [plateapi_rego_search] shortcode:
<?php
/**
* Plugin Name: PlateAPI Rego Search
* Description: Number plate search that lists the products fitting the customer's vehicle.
* Version: 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
// Add your key to wp-config.php, never to the theme or to JavaScript:
// define('PLATEAPI_KEY', 'pk_live_...');
const PLATEAPI_META_KEY = '_plateapi_vehicle_ids';
const PLATEAPI_STATES = ['ACT', 'NSW', 'NT', 'QLD', 'SA', 'TAS', 'VIC', 'WA'];
const PLATEAPI_SEARCHES_PER_HOUR = 10;
add_action('rest_api_init', function () {
register_rest_route('plateapi/v1', '/lookup', [
'methods' => 'GET',
'callback' => 'plateapi_rest_lookup',
'permission_callback' => '__return_true',
]);
register_rest_route('plateapi/v1', '/products', [
'methods' => 'GET',
'callback' => 'plateapi_rest_products',
'permission_callback' => '__return_true',
]);
});
function plateapi_rest_lookup(WP_REST_Request $request) {
$plate = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', (string) $request->get_param('plate')));
$state = strtoupper((string) $request->get_param('state'));
if ($plate === '' || strlen($plate) > 10 || !in_array($state, PLATEAPI_STATES, true)) {
return new WP_REST_Response(['error' => 'Enter a plate and choose a state.'], 400);
}
// A plate searched again within a day is answered from the cache: no new lookup.
$cache_key = 'plateapi_' . md5($state . '|' . $plate);
$cached = get_transient($cache_key);
if ($cached !== false) {
return new WP_REST_Response($cached, 200);
}
// Each new plate is a billed lookup, so limit how many one visitor can run.
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
$limit_key = 'plateapi_rl_' . md5($ip);
$count = (int) get_transient($limit_key);
if ($count >= PLATEAPI_SEARCHES_PER_HOUR) {
return new WP_REST_Response(['error' => 'Too many searches. Try again later.'], 429);
}
set_transient($limit_key, $count + 1, HOUR_IN_SECONDS);
$url = 'https://api.plateapi.com.au/api/v1/lookup?plate=' . rawurlencode($plate)
. '&state=' . rawurlencode($state);
$response = wp_remote_get($url, [
'headers' => ['X-API-Key' => PLATEAPI_KEY],
'timeout' => 20,
]);
$status = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response);
$data = $status === 200 ? json_decode(wp_remote_retrieve_body($response), true) : null;
// 503 lookup_failed and 200 internal_error are not billed: ask the customer to retry,
// and do not cache the answer.
if (!is_array($data) || (isset($data['code']) && $data['code'] === 'internal_error')) {
return new WP_REST_Response(['error' => 'Lookup unavailable. Please try again.'], 503);
}
if (empty($data['success'])) {
$result = ['found' => false];
} else {
$result = [
'found' => true,
'vehicle' => $data['vehicle']['description'],
'match' => $data['match'],
'candidates' => plateapi_candidates($data),
];
}
set_transient($cache_key, $result, DAY_IN_SECONDS);
return new WP_REST_Response($result, 200);
}
// Every vehicle the plate could be, each with its PlateAPI vehicle_id. With match
// "ambiguous" the vehicle has no ID and alternatives already lists every candidate.
function plateapi_candidates(array $data) {
$list = $data['match'] === 'ambiguous'
? $data['alternatives']
: array_merge([$data['vehicle']], $data['alternatives']);
$candidates = [];
foreach ($list as $vehicle) {
if (!empty($vehicle['vehicle_id'])) {
$candidates[] = [
'vehicle_id' => (int) $vehicle['vehicle_id'],
'label' => $vehicle['description'],
];
}
}
return $candidates;
}
function plateapi_rest_products(WP_REST_Request $request) {
$vehicle_id = absint($request->get_param('vehicle_id'));
if ($vehicle_id === 0) {
return new WP_REST_Response(['error' => 'Missing vehicle_id.'], 400);
}
// The custom field holds a comma-separated list of IDs, e.g. "1380801,1346446".
$query = new WP_Query([
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => 50,
'fields' => 'ids',
'meta_query' => [[
'key' => PLATEAPI_META_KEY,
'value' => '(^|,) *' . $vehicle_id . ' *(,|$)',
'compare' => 'REGEXP',
]],
]);
$products = [];
foreach ($query->posts as $id) {
$product = wc_get_product($id);
if ($product && $product->is_visible()) {
$products[] = ['name' => $product->get_name(), 'url' => get_permalink($id)];
}
}
return new WP_REST_Response(['products' => $products], 200);
}
add_shortcode('plateapi_rego_search', function () {
wp_enqueue_script('plateapi-rego', plugins_url('rego.js', __FILE__), [], '1.0.0', true);
wp_localize_script('plateapi-rego', 'plateapiRego', ['api' => esc_url_raw(rest_url('plateapi/v1/'))]);
$options = '';
foreach (PLATEAPI_STATES as $state) {
$options .= '<option value="' . esc_attr($state) . '">' . esc_html($state) . '</option>';
}
return '<form class="plateapi-rego">'
. '<label>Rego <input name="plate" maxlength="10" autocomplete="off" required></label> '
. '<label>State <select name="state">' . $options . '</select></label> '
. '<button type="submit">Find parts</button>'
. '<div class="plateapi-rego__result" aria-live="polite"></div>'
. '</form>';
});The second file runs in the browser. It sends the form to your site, shows a picker when there is more than one candidate, and lists the matching products:
document.querySelectorAll('.plateapi-rego').forEach((form) => {
const out = form.querySelector('.plateapi-rego__result');
async function getJson(path) {
const res = await fetch(plateapiRego.api + path);
return { status: res.status, body: await res.json() };
}
async function showParts(vehicle) {
try {
localStorage.setItem('plateapiVehicle', JSON.stringify(vehicle));
} catch (e) {}
out.textContent = 'Parts for ' + vehicle.label + ':';
const { body } = await getJson('products?vehicle_id=' + vehicle.vehicle_id);
if (!body.products || body.products.length === 0) {
out.textContent = 'No parts listed yet for ' + vehicle.label + '.';
return;
}
const list = document.createElement('ul');
for (const product of body.products) {
const link = document.createElement('a');
link.href = product.url;
link.textContent = product.name;
list.appendChild(document.createElement('li')).appendChild(link);
}
out.appendChild(list);
}
function showPicker(candidates) {
out.textContent = 'Which one is yours?';
for (const vehicle of candidates) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = vehicle.label;
button.addEventListener('click', () => showParts(vehicle));
out.appendChild(button);
}
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
out.textContent = 'Looking up...';
const plate = encodeURIComponent(form.elements.plate.value);
const state = encodeURIComponent(form.elements.state.value);
try {
const { status, body } = await getJson('lookup?plate=' + plate + '&state=' + state);
if (status !== 200) {
out.textContent = body.error || 'Lookup unavailable. Please try again.';
} else if (!body.found) {
out.textContent = 'No vehicle found. Check the plate and state.';
} else if (body.candidates.length === 0) {
out.textContent = 'Found ' + body.vehicle + ', but it could not be matched to parts automatically.';
} else if (body.candidates.length === 1) {
showParts(body.candidates[0]);
} else {
showPicker(body.candidates);
}
} catch (e) {
out.textContent = 'Lookup unavailable. Please try again.';
}
});
// Returning customer: show the vehicle they picked last time.
try {
const saved = JSON.parse(localStorage.getItem('plateapiVehicle'));
if (saved && saved.vehicle_id) {
showParts(saved);
}
} catch (e) {}
});Activate "PlateAPI Rego Search" under Plugins, then add the shortcode [plateapi_rego_search] to any page, or to your shop sidebar with a Shortcode block.
Step 4: test it
Search for TEST123 with any state. The lookup returns the sandbox vehicle, a 2015-2020 Toyota Hilux SR 2.8L diesel with ID 1380801, and the page lists the product you tagged in step 1. Sandbox lookups are free and do not count toward your quota. You can also call the route directly to see what the browser receives:
curl -s "https://your-store.com.au/wp-json/plateapi/v1/lookup?plate=TEST123&state=NSW"
{
"found": true,
"vehicle": "15~20 TOYOTA HILUX SR 2.8L DIESEL",
"match": "exact",
"candidates": [
{ "vehicle_id": 1380801, "label": "15~20 TOYOTA HILUX SR 2.8L DIESEL" }
]
}How the plugin handles the edge cases
- Several possible vehicles. When the lookup's
matchis"multiple"(trims of one model) or"ambiguous"(different models or generations), every candidate comes back with its ownvehicle_idand the customer picks theirs. If you would rather show everything that fits any candidate, query each ID and merge the lists. - No certain variant. A vehicle without a
vehicle_idmeans PlateAPI could not identify the exact variant, and it never guesses one. The plugin shows the vehicle description and says it cannot match parts automatically; that is a good place for a make/model/year menu. - Not found. A plate that does not resolve is a normal answer (HTTP 200) and counts as a lookup, so the plugin strips spaces and symbols from the plate first, and caches the answer like any other.
- Temporary failures. A
503withlookup_failed, or a200withinternal_error, is not billed. The plugin asks the customer to try again and does not cache it. - Repeat searches. Results are cached for a day per plate and state, so the same plate searched again costs nothing.
- Bots. Every new plate is a billed lookup, so each visitor is limited to 10 new plates an hour. If your site sits behind Cloudflare or another proxy,
REMOTE_ADDRis the proxy's address: use the client IP header your host documents instead.
Where to go next
- Add a make, model and year menu for customers without their plate handy, using the vehicle database cascade (Starter and above). It returns the same vehicle IDs, so the same product field works for both.
- Show the saved vehicle in the site header and filter category pages by it.
- Read the lookup reference for every response field, or try a real plate on the live demo.
PlateAPI turns Australian number plates into structured vehicle data -- one REST endpoint, all eight states, free tier included.