Vehicle Database

Minimum plan for access: Starter

The vehicle database (/api/v1/vehicles and /api/v1/vehicles/{vehicle_id}) is available on Starter plans and above. Free and Lite keys receive 403. See plans.

Acceptable use: bulk retrieval of the vehicle database is not permitted, including programmatically iterating through the make, model and year cascade to copy or replicate the dataset. Usage is monitored by automated systems. Accounts found doing this will have their vehicle database access permanently revoked without a refund, as set out in our Terms of Service. Plate lookup access is not affected. Populating vehicle selectors, retrieving records for vehicles returned by your lookups and mapping your own product catalogue to vehicle IDs are all permitted.

GET/api/v1/vehicles

What this is for

The vehicle database has two uses. First, make / model / year dropdowns: a parts store can let a customer pick their vehicle without a plate number, narrowing from make down to an exact variant. Second, resolving a plate lookup: every vehicle_id returned by /api/v1/lookup can be fetched here as a full record (series, engine, variant, year range). The database holds 23,000+ Australian market vehicle variants across 225+ makes (125,000+ year-level records), cars to trucks and buses.

NEWMotorcycles have their own cascade: add type=motorcycle to any /api/v1/vehicles request for 15,000+ motorcycles, scooters, ATVs and snowmobiles across 420+ makes (Honda, Yamaha, Kawasaki, Suzuki, Harley Davidson, KTM, Polaris ...). Without type, or with type=car, you get the car cascade, unchanged. Same seven levels, same rules and the same kind of vehicle_id: a motorcycle plate lookup returns the same ID as the motorcycle cascade, and /api/v1/vehicles/{vehicle_id} returns either kind, with vehicle_type saying which. Some older bikes have no year information: they have an ID and a full record with years empty and lowest_year null, but do not appear in the year lists.

Available on Starter plans and above (not Free or Lite). Vehicle database requests do not count toward your monthly lookup quota and have their own limit of 500 requests per minute.

Quick start in 60 seconds

Replace pk_live_YOUR_API_KEY with a Starter-or-above key and run these three commands in order.

1. List every make:

shell
curl -s "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"
200 OK -- makes (trimmed)
{
  "success": true,
  "data": [
    {
      "type": "make",
      "data": ["ABARTH", "ACE EV", "ACURA", "ALEXANDER DENNIS", "ALFA ROMEO", "ALPINE", "..."]
    }
  ],
  "total": 227,
  "duration_ms": 19.1
}

2. Pick a make from that list and ask for its models:

shell
curl -s "https://api.plateapi.com.au/api/v1/vehicles?make=TOYOTA" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"
200 OK -- models (trimmed)
{
  "success": true,
  "data": [
    {
      "type": "model",
      "data": ["4 RUNNER", "4 RUNNER DELUXE", "4 RUNNER RV6, V6 SR5", "4 RUNNER SR5", "86 GT", "86 GT, GTS", "..."]
    }
  ],
  "total": 328,
  "duration_ms": 4.67
}

3. Fetch one full vehicle record by ID. 1380801 is the vehicle returned by the sandbox plate TEST123:

shell
curl -s "https://api.plateapi.com.au/api/v1/vehicles/1380801" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"
200 OK -- vehicle record (trimmed)
{
  "success": true,
  "vehicle": {
    "vehicle_id": 1380801,
    "make": "TOYOTA",
    "model": "HILUX SR",
    "year_range": "2015-2020",
    "series": "GUN126R -GUN126",
    "engine": "2.8L,  DIE 1GDFTV I4 16v DOHC I/C Turbo CRD {130kW}",
    "variant": "SR,  4D Ute -, 4WD MR0KA3CD  [THAILAND], AT/MT",
    "detail": "full",
    ...
  },
  "duration_ms": 1.61
}

How the cascade works

One endpoint, seven levels. Each request adds one more parameter and gets back the list for the next level. Every successful response has the same shape: {"success": true, "data": [{"type": "<level>", "data": [...]}], "total": N, "duration_ms": x}. The type field tells you which level you got.

If the vehicle database is briefly unavailable, any level returns 503 with a Retry-After header and {"success": false, "code": "unavailable", "data": [], "total": 0}, never a 200. So a 200 with an empty list always means a value that does not exist, not an outage.

LevelParameters you sendtype returneddata containsExample value
1nonemakeMakesTOYOTA
2makemodelModels for the makeHILUX
3+ modelyearYears, newest first (numbers)2019
4+ yearseriesSeries / chassis codesGUN126R -GUN126
5+ seriesengineEngine descriptions2.8L, ... {130kW}
6+ enginevariantVariant names (select_text)4D Ute -, ... AT/MT
7+ variantvehicleVehicle records with vehicle_id1346446

Four rules cover almost every integration problem:

  • Pass values exactly as returned, and URL-encode them. Values contain commas, double spaces, braces, brackets and slashes. Copy the string from the previous response without trimming or tidying it. Matching is exact, including letter case: make=toyota or model=Hilux returns an empty list, and so does a double space collapsed to a single space. Neither is an error. Let your HTTP library do the encoding (curl --data-urlencode, URLSearchParams, requests params=).
  • An empty-string series is a real value. Some vehicles have no series code, so the series list contains "". Select it like any other value and send series= (empty) to continue to engines.
  • Unknown values are not errors. A make, model or other value that does not exist returns 200 with an empty data list. If you get an empty list, check that you passed the previous response's value unchanged.
  • Parameters build on each other, in order. A later parameter without the earlier ones is ignored: sending model without make returns the makes list, year without model returns the models. year must be a whole number between 1900 and 2100 and text values are limited to 200 characters; anything else returns 400.

Every list contains only vehicles that have a full database record, so any path that reaches level 7 returns at least one vehicle with a vehicle_id. You can stop at any level: a make/model/year picker only needs levels 1 to 3.

Level 7 usually returns exactly one vehicle, but treat it as a list. Fewer than 100 paths return two: different vehicles whose records overlap in the year you picked and share every text value. For example KIA / SELTOS / 2022 / EP81A / 2.0L ... CVT returns 5434441 (2019-2022) and 2573335 (2022 on). Show both, or fetch each record by ID and use its year_range to tell them apart.

Headers

HeaderRequiredDescription
X-API-KeyRequiredAn API key on a Starter plan or above. Free and Lite keys receive 403 on this endpoint.

Query parameters

ParameterTypeRequiredDescription
type NEWstringOptionalcar (default) or motorcycle: which cascade to query. Any other value returns 400.
makestringOptionalManufacturer name exactly as returned by the makes list (e.g. TOYOTA)
modelstringOptionalModel name exactly as returned by the models list. Requires make.
yearintegerOptionalModel year as returned by the years list. Requires make and model.
seriesstringOptionalSeries/chassis code as returned by the series list. Can be an empty string for vehicles without a series code. Requires make, model, and year.
enginestringOptionalEngine description as returned by the engines list. Requires all previous parameters.
variantstringOptionalVariant name (select_text) as returned by the variants list. Requires all previous parameters. Returns full vehicle records.

Full walkthrough: Toyota Hilux 2019

Seven requests take you from the makes list to a vehicle_id. Each command below is copy-paste runnable. We use curl -G --data-urlencode so you can write the raw values and let curl encode them; -G sends them as a query string.

Step 1 -- no parameters returns every make:

shell
curl -s -G "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"
200 OK -- makes (trimmed)
{
  "success": true,
  "data": [
    {
      "type": "make",
      "data": ["ABARTH", "ACE EV", "ACURA", "ALEXANDER DENNIS", "ALFA ROMEO", "ALPINE", "..."]
    }
  ],
  "total": 227,
  "duration_ms": 19.1
}

Step 2 -- add make to list that make's models:

shell
curl -s -G "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY" \
  --data-urlencode "make=TOYOTA"
200 OK -- models (trimmed)
{
  "success": true,
  "data": [
    {
      "type": "model",
      "data": ["4 RUNNER", "4 RUNNER DELUXE", "4 RUNNER RV6, V6 SR5", "4 RUNNER SR5", "86 GT", "86 GT, GTS", "..."]
    }
  ],
  "total": 328,
  "duration_ms": 4.67
}

Step 3 -- add model to list its years, newest first:

shell
curl -s -G "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY" \
  --data-urlencode "make=TOYOTA" \
  --data-urlencode "model=HILUX"
200 OK -- years (trimmed)
{
  "success": true,
  "data": [
    {
      "type": "year",
      "data": [2025, 2024, 2023, 2022, 2021, 2020, "...", 1969, 1968]
    }
  ],
  "total": 58,
  "duration_ms": 1.05
}

Step 4 -- add year to list series/chassis codes. Some vehicles have an empty string for series (see the rules above):

shell
curl -s -G "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY" \
  --data-urlencode "make=TOYOTA" \
  --data-urlencode "model=HILUX" \
  --data-urlencode "year=2019"
200 OK -- series
{
  "success": true,
  "data": [
    {
      "type": "series",
      "data": ["GUN126R -GUN126", "GUN136R -GUN136"]
    }
  ],
  "total": 2,
  "duration_ms": 0.82
}

Step 5 -- add series to list engines. For a vehicle with no series code you would send --data-urlencode "series=":

shell
curl -s -G "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY" \
  --data-urlencode "make=TOYOTA" \
  --data-urlencode "model=HILUX" \
  --data-urlencode "year=2019" \
  --data-urlencode "series=GUN126R -GUN126"
200 OK -- engines
{
  "success": true,
  "data": [
    {
      "type": "engine",
      "data": ["2.8L,  DIE 1GDFTV I4 16v DOHC I/C Turbo CRD {130kW}"]
    }
  ],
  "total": 1,
  "duration_ms": 0.95
}

Note the two spaces after 2.8L,. They are part of the value and must be sent as-is.

Step 6 -- add engine to list variant names:

shell
curl -s -G "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY" \
  --data-urlencode "make=TOYOTA" \
  --data-urlencode "model=HILUX" \
  --data-urlencode "year=2019" \
  --data-urlencode "series=GUN126R -GUN126" \
  --data-urlencode "engine=2.8L,  DIE 1GDFTV I4 16v DOHC I/C Turbo CRD {130kW}"
200 OK -- variant names
{
  "success": true,
  "data": [
    {
      "type": "variant",
      "data": ["4D Ute -, 4WD MR0HA3CD  [THAILAND], AT/MT"]
    }
  ],
  "total": 1,
  "duration_ms": 0.69
}

Step 7 -- add variant to get the vehicle records. The two spaces before [THAILAND] are part of the value:

shell
curl -s -G "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY" \
  --data-urlencode "make=TOYOTA" \
  --data-urlencode "model=HILUX" \
  --data-urlencode "year=2019" \
  --data-urlencode "series=GUN126R -GUN126" \
  --data-urlencode "engine=2.8L,  DIE 1GDFTV I4 16v DOHC I/C Turbo CRD {130kW}" \
  --data-urlencode "variant=4D Ute -, 4WD MR0HA3CD  [THAILAND], AT/MT"
200 OK -- vehicle records
{
  "success": true,
  "data": [
    {
      "type": "vehicle",
      "data": [
        {
          "vehicle_id": 1346446,
          "select_text": "4D Ute -, 4WD MR0HA3CD  [THAILAND], AT/MT",
          "description": "15~20 TOYOTA HILUX 2.8L DIESEL",
          "long_description": "TOYOTA HILUX 10/2015~04/2020"
        }
      ]
    }
  ],
  "total": 1,
  "duration_ms": 1.58
}

If you prefer to build the URL yourself, this is the same step 7 request with every value percent-encoded (the result is identical):

shell
curl -s "https://api.plateapi.com.au/api/v1/vehicles?make=TOYOTA&model=HILUX&year=2019\
&series=GUN126R%20-GUN126\
&engine=2.8L%2C%20%20DIE%201GDFTV%20I4%2016v%20DOHC%20I%2FC%20Turbo%20CRD%20%7B130kW%7D\
&variant=4D%20Ute%20-%2C%204WD%20MR0HA3CD%20%20%5BTHAILAND%5D%2C%20AT%2FMT" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"

vehicle_id identifies the variant, not the year: the same variant returns the same ID for every model year it was sold. It is the same ID that /api/v1/lookup returns on a vehicle, and the key for /api/v1/vehicles/{vehicle_id}.

Motorcycle walkthrough: Yamaha MT-07 2021 NEW

Exactly the same as the car walkthrough above, but add type=motorcycle to every request. Leave it out on any step and you are back in the car cascade: make=YAMAHA without type returns an empty list (Yamaha makes no cars here), and make=HONDA returns Honda's cars, not its bikes.

Step 1 -- the motorcycle makes:

shell
curl -s -G "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY" \
  --data-urlencode "type=motorcycle"
200 OK -- makes (trimmed)
{
  "success": true,
  "data": [
    {
      "type": "make",
      "data": ["ACCESS", "ACCOSSATO", "ADIVA", "ADLER", "ADLY", "AEON", "..."]
    }
  ],
  "total": 428,
  "duration_ms": 23.36
}

Steps 2 to 6 work as for the Hilux, one parameter at a time. Motorcycles usually have a frame code as their series (or an empty one), and the series level matters: the 2021 MT-07 has six, the full-power HOA (55 kW) and the learner-approved LA (38 kW) versions, each with a new frame code from mid-2021. Step 7, the 2021-22 full-power bike:

shell
curl -s -G "https://api.plateapi.com.au/api/v1/vehicles" \
  -H "X-API-Key: pk_live_YOUR_API_KEY" \
  --data-urlencode "type=motorcycle" \
  --data-urlencode "make=YAMAHA" \
  --data-urlencode "model=MT-07" \
  --data-urlencode "year=2021" \
  --data-urlencode "series=RM332" \
  --data-urlencode "engine=689cc, M419E I2 8v DOHC CARB {55kW}" \
  --data-urlencode "variant=HOA,  2 Wheel Road, RWD, MT"
200 OK -- vehicle records
{
  "success": true,
  "data": [
    {
      "type": "vehicle",
      "data": [
        {
          "vehicle_id": 7076553,
          "select_text": "HOA,  2 Wheel Road, RWD, MT",
          "description": "21~22 YAMAHA MT-07 0.7L",
          "long_description": "YAMAHA MT-07 HOA . RM332 07/2021 ~ 12/2022 2 Wheels PETROL 0.7 litre, M419E I2 8v DOHC CARB {55KW}"
        }
      ]
    }
  ],
  "total": 1,
  "duration_ms": 1.64
}

The same walk in code. Each call adds type for you, so the rest is the car walk unchanged:

python
import requests

API = "https://api.plateapi.com.au/api/v1/vehicles"
HEADERS = {"X-API-Key": "pk_live_YOUR_API_KEY"}


def level(**params):
    """One level of the motorcycle cascade."""
    r = requests.get(API, params={"type": "motorcycle", **params}, headers=HEADERS, timeout=10)
    r.raise_for_status()
    return r.json()["data"][0]["data"]


path = {"make": "YAMAHA", "model": "MT-07", "year": 2021}
print(level(**path))                  # ['RM044', 'RM04F', 'RM04G', 'RM161', 'RM332', 'RM338']
path["series"] = "RM332"
path["engine"] = level(**path)[0]     # '689cc, M419E I2 8v DOHC CARB {55kW}'
path["variant"] = level(**path)[0]    # 'HOA,  2 Wheel Road, RWD, MT'
for v in level(**path):
    print(v["vehicle_id"], v["description"])   # 7076553 21~22 YAMAHA MT-07 0.7L
javascript (Node 18+, save as walk.mjs)
const API = "https://api.plateapi.com.au/api/v1/vehicles";

async function level(params) {
  const qs = new URLSearchParams({ type: "motorcycle", ...params });
  const r = await fetch(API + "?" + qs, {
    headers: { "X-API-Key": process.env.PLATEAPI_KEY },
  });
  const body = await r.json();
  return body.data[0].data;
}

const path = { make: "YAMAHA", model: "MT-07", year: "2021", series: "RM332" };
path.engine = (await level(path))[0];    // "689cc, M419E I2 8v DOHC CARB {55kW}"
path.variant = (await level(path))[0];   // "HOA,  2 Wheel Road, RWD, MT"
const [bike] = await level(path);
console.log(bike.vehicle_id, bike.description);   // 7076553 21~22 YAMAHA MT-07 0.7L

The ID works everywhere a car's does. /api/v1/vehicles/{vehicle_id} needs no type and tells you which kind it is, and a motorcycle plate lookup returns the same ID as the motorcycle cascade:

shell
curl -s "https://api.plateapi.com.au/api/v1/vehicles/7076553" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"
200 OK -- motorcycle record (trimmed)
{
  "success": true,
  "vehicle": {
    "vehicle_id": 7076553,
    "vehicle_type": "motorcycle",
    "make": "YAMAHA",
    "model": "MT-07",
    "year_range": "2021-2022",
    "years": [2021, 2022],
    "series": "RM332",
    "engine": "689cc, M419E I2 8v DOHC CARB {55kW}",
    "variant": "HOA,  2 Wheel Road, RWD, MT",
    "drive": "RWD",
    "transmission": "MT",
    "detail": "full",
    ...
  },
  "duration_ms": 2.09
}

Building dropdowns

The browser example below drives six <select> elements and shows the final vehicle records. It calls your own backend at /api/vehicles, not PlateAPI directly, because anything in browser JavaScript is visible to your visitors and would expose your API key. The backend adds the key and forwards the query:

javascript (your backend, Node 18+ / Express)
app.get("/api/vehicles", async (req, res) => {
  // Forwards every parameter, type included, so one backend serves car and
  // motorcycle dropdowns. Optional: only forward the seven known parameters.
  const qs = new URLSearchParams(req.query);
  const r = await fetch("https://api.plateapi.com.au/api/v1/vehicles?" + qs, {
    headers: { "X-API-Key": process.env.PLATEAPI_KEY },
  });
  res.status(r.status).json(await r.json());
});

The page markup is six selects with ids make, model, year, series, engine, variant and an empty <ul id="result">. Each change re-fetches the next level; the empty series is shown as "(no series code)" and sent back as series=.

javascript (browser)
const ORDER = ["make", "model", "year", "series", "engine", "variant"];
const selects = ORDER.map(id => document.getElementById(id));
const result = document.getElementById("result");

async function level(params) {
  const res = await fetch("/api/vehicles?" + new URLSearchParams(params));
  if (!res.ok) throw new Error("HTTP " + res.status);
  const json = await res.json();
  if (!json.success) throw new Error(json.error || "query failed");
  return json.data[0];                        // { type, data }
}

function fill(select, values) {
  select.replaceChildren(new Option("Select...", ""));
  for (const v of values) {
    select.add(new Option(v === "" ? "(no series code)" : v, v));
  }
  select.disabled = false;
}

selects.forEach((select, i) => {
  select.addEventListener("change", async () => {
    selects.slice(i + 1).forEach(s => { s.replaceChildren(); s.disabled = true; });
    result.replaceChildren();
    if (select.selectedIndex === 0) return;   // placeholder chosen
    const params = {};                         // everything chosen so far
    for (let j = 0; j <= i; j++) params[ORDER[j]] = selects[j].value;
    const { type, data } = await level(params);
    if (type === "vehicle") {                  // after the variant: final records
      for (const v of data) {
        const li = document.createElement("li");
        li.textContent = v.vehicle_id + " -- " + v.select_text;
        result.append(li);
      }
    } else {
      fill(selects[i + 1], data);
    }
  });
});

selects.forEach(s => { s.disabled = true; });
level({}).then(r => fill(selects[0], r.data)); // load the makes

The same walk in Python. This one reads the key from an environment variable and takes the first option at each level; in a real integration you would let the user choose or match on select_text:

python
import os
import requests

API = "https://api.plateapi.com.au/api/v1/vehicles"
HEADERS = {"X-API-Key": os.environ["PLATEAPI_KEY"]}

def level(**params):
    r = requests.get(API, params=params, headers=HEADERS, timeout=10)
    r.raise_for_status()
    return r.json()["data"][0]["data"]    # requests URL-encodes the values

params = {"make": "TOYOTA", "model": "HILUX", "year": 2019}
for key in ("series", "engine", "variant"):
    params[key] = level(**params)[0]      # first option; "" is a valid value
    print(key, "->", repr(params[key]))

for v in level(**params):                 # final level: vehicle records
    print(v["vehicle_id"], v["select_text"])

From a plate to a database record NEW

A plate lookup returns a vehicle_id on vehicle and on each entry in alternatives. Pass it to GET /api/v1/vehicles/{vehicle_id} to get the full record, with no quota cost:

shell
# 1. Look up the plate (counts toward quota; TEST123 is free)
curl -s "https://api.plateapi.com.au/api/v1/lookup?plate=TEST123&state=NSW" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"
# ... "vehicle": { "vehicle_id": 1380801, ... }

# 2. Fetch the full record for that ID
curl -s "https://api.plateapi.com.au/api/v1/vehicles/1380801" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"
  • When the lookup's match is "multiple", the plate could be several variants. Either store every candidate vehicle_id (the vehicle and all alternatives), for example to show parts that fit any of them, or ask the customer to pick one.
  • vehicle_id is null when the variant could not be identified with certainty. An ID is never guessed.
  • The ID is stable and identifies the variant, not the year, so it is safe to store against a customer's garage or an order. The record's years list tells you which model years the variant covers.

Linking your products (SKUs) to vehicles NEW

The point of a stable vehicle_id is that you tag each product once with the vehicles it fits, and from then on a plate lookup tells you which products to show. Three steps: a fitment table, a one-off mapping of your existing catalogue, and a query on every lookup.

1. A fitment table. Many-to-many between your products and vehicle_ids. One ID covers every model year of that variant, so a part that fits a 2015-2020 Hilux SR needs one row per variant, not one per year.

sql
CREATE TABLE fitments (
  sku         text    NOT NULL,
  vehicle_id  integer NOT NULL,   -- PlateAPI vehicle_id
  PRIMARY KEY (sku, vehicle_id)
);

2. Map your existing catalogue once. Most catalogues describe fitment as text ("Toyota Hilux 2015-2020 2.8L diesel"). Resolve each description to IDs by walking the cascade: every model name that starts with the base model (the database files trims as separate models: HILUX, HILUX SR, HILUX SR5 EXTRA CAB ...), each year in the range, every series, the engines that match, and the vehicles under them. This script does exactly that:

python -- resolve fitment text to vehicle_ids
import os, time, requests

API = "https://api.plateapi.com.au/api/v1/vehicles"
HEADERS = {"X-API-Key": os.environ["PLATEAPI_KEY"]}

def level(**params):
    r = requests.get(API, params=params, headers=HEADERS, timeout=15)
    r.raise_for_status()
    body = r.json()
    if not body["success"]:
        raise RuntimeError(body.get("error", "query failed"))
    time.sleep(0.15)                      # stays under 500 requests/minute
    return body["data"][0]["data"]

def vehicle_ids(make, base_model, year_from, year_to, engine_contains=""):
    """Every vehicle_id for a make, all model names starting with base_model,
    a year range, and engines containing a substring. One ID per variant."""
    ids = {}
    models = [m for m in level(make=make)
              if m == base_model or m.startswith(base_model + " ")]
    for model in models:
        years = [y for y in level(make=make, model=model) if year_from <= y <= year_to]
        for year in years:
            for series in level(make=make, model=model, year=year):
                for engine in level(make=make, model=model, year=year, series=series):
                    if engine_contains.upper() not in engine.upper():
                        continue
                    for variant in level(make=make, model=model, year=year,
                                         series=series, engine=engine):
                        for v in level(make=make, model=model, year=year, series=series,
                                       engine=engine, variant=variant):
                            ids[v["vehicle_id"]] = model + " / " + v["select_text"]
    return ids

ids = vehicle_ids("TOYOTA", "HILUX", 2015, 2020, engine_contains="2.8L")
for vehicle_id, text in sorted(ids.items()):
    print(vehicle_id, text)
# then insert one fitments row per (sku, vehicle_id)
output (run 2026-10-01: 26 model names, 21 vehicle_ids, 349 requests, 82 seconds)
1322020 HILUX SR5 EXTRA CAB / SR5 EXTRA CAB,  4D Ute -, 4WD MR0HA3CC  [THAILAND], AT/MT
1330126 HILUX SR5 / SR5,  4D Ute -, 4WD MR0HA3CD  [THAILAND], AT/MT
1346446 HILUX / 4D Ute -, 4WD MR0HA3CD  [THAILAND], AT/MT
1380801 HILUX SR / SR,  4D Ute -, 4WD MR0KA3CD  [THAILAND], AT/MT
1428997 HILUX / 4D Ute -, RWD MR0AA3DD  [THAILAND], AT
1515123 HILUX SR5 HI-RIDER / SR5 HI-RIDER,  4D Ute -, RWD MR0GA3DD  [THAILAND], AT/MT
...
  • Cache the levels you have already fetched (makes, each make's models, each model's years). The same nodes recur across products, so a cached walk makes far fewer requests.
  • Map a large catalogue in batches. Mapping your own product catalogue is a permitted use of the vehicle database. Run it in batches over time rather than as one continuous job, and contact support before mapping a very large catalogue so we can help.
  • Narrow by engine, body or drive when a part is specific. The engine string carries displacement and code; select_text carries doors, body, drive and gearbox ("4D Ute", "4WD", "AT/MT").
  • Map every ID the walk returns. The text alone does not tell variants apart: several of the 21 above share identical text but are different vehicles (the 2015-2020 130kW and 2020-2025 150kW versions of one trim), and a lookup returns whichever one the plate is.
  • Store the IDs, not the text. IDs are stable; names and series strings can change.
  • Import format: a two-column CSV, sku,vehicle_id, is all you need.

3. On every lookup, query the table.

sql -- products that fit the looked-up vehicle
-- lookup response: vehicle.vehicle_id = 1380801, match = "exact"
SELECT sku FROM fitments WHERE vehicle_id = 1380801;

-- match = "multiple": the plate could be several variants.
-- Either show everything that fits any candidate ...
SELECT DISTINCT sku FROM fitments
WHERE vehicle_id IN (1380801, 1346446, 1330126);
-- ... or show the candidates' descriptions, let the customer confirm
-- their variant, and query the one ID they chose.

Keeping it current. New variants get new IDs as they are added; a vehicle first seen on a plate starts as a basic record and keeps its ID when the full record arrives. Re-run the mapping for products whose fitment text has not been resolved yet (monthly is plenty) and never re-map rows you already have. If a lookup returns a vehicle_id with no fitments row, that is a gap in your mapping, not an unknown vehicle: fetch the record by ID and map the SKU.

Fetch one vehicle by ID NEW

GET/api/v1/vehicles/{vehicle_id}

Returns the record for one vehicle_id. This is how a plate lookup links to the vehicle database: take the vehicle_id from a lookup response and fetch its record here. Same auth and rate limit as the cascade, and no quota consumption.

shell
curl -s "https://api.plateapi.com.au/api/v1/vehicles/1380801" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"
200 OK -- vehicle record
{
  "success": true,
  "vehicle": {
    "vehicle_id": 1380801,
    "vehicle_type": "car",
    "make": "TOYOTA",
    "model": "HILUX SR",
    "year_range": "2015-2020",
    "lowest_year": 2015,
    "highest_year": 2020,
    "years": [2015, 2016, 2017, 2018, 2019, 2020],
    "description": "15~20 TOYOTA HILUX 2.8L DIESEL",
    "long_description": "TOYOTA HILUX 10/2015~04/2020",
    "series": "GUN126R -GUN126",
    "engine": "2.8L,  DIE 1GDFTV I4 16v DOHC I/C Turbo CRD {130kW}",
    "variant": "SR,  4D Ute -, 4WD MR0KA3CD  [THAILAND], AT/MT",
    "body": "UTE",
    "body_size": null,
    "doors": 4,
    "drive": "4WD",
    "transmission": "AT/MT",
    "detail": "full"
  },
  "duration_ms": 1.2
}

body, body_size (SUVs: COMPACT, MEDIUM, FULL-SIZE), doors, drive (4WD, AWD, RWD, FWD, 6X4 ...) and transmission (AT, MT, AT/MT, CVT) are read out of the variant name and are null when it does not carry them. If the ID you request has been merged into another (the database occasionally held one vehicle under two IDs), the response is the current record: compare its vehicle_id with the one you sent and update what you stored. vehicle_type is car or motorcycle (which cascade the vehicle is in).

detail is "full" for a complete record. It is "basic" for a vehicle that has an ID but whose full record has not been added yet: make, model, year range, description and engine displacement are filled, while series, variant and long_description are null and years is empty. The ID does not change when the full record is added. A basic record (a 1993 Toyota Camry):

shell
curl -s "https://api.plateapi.com.au/api/v1/vehicles/7295503" \
  -H "X-API-Key: pk_live_YOUR_API_KEY"
200 OK -- basic record
{
  "success": true,
  "vehicle": {
    "vehicle_id": 7295503,
    "vehicle_type": "car",
    "make": "TOYOTA",
    "model": "CAMRY",
    "year_range": "1993-1997",
    "lowest_year": 1993,
    "highest_year": 1997,
    "years": [],
    "description": "93~97 TOYOTA CAMRY 2.2L PETROL",
    "long_description": null,
    "series": null,
    "engine": "2.2L",
    "variant": null,
    "body": null,
    "body_size": null,
    "doors": null,
    "drive": null,
    "transmission": null,
    "detail": "basic"
  },
  "duration_ms": 1.58
}

Errors

404 Not Found -- no vehicle with that ID
{
  "success": false,
  "vehicle": null,
  "code": "not_found",
  "error": "No vehicle with that vehicle_id.",
  "duration_ms": 0.49
}
400 Bad Request -- non-numeric ID (e.g. /vehicles/abc)
{
  "detail": "Invalid path parameters. vehicle_id: Input should be a valid integer, unable to parse string as an integer"
}
503 Service Unavailable -- Retry-After header set
{
  "success": false,
  "vehicle": null,
  "code": "unavailable",
  "error": "Vehicle database temporarily unavailable. Please retry.",
  "duration_ms": 0.3
}

A 503 means the vehicle database is briefly unavailable: wait for the Retry-After interval and retry. The cascade answers an outage the same way, with "data": [], "total": 0 in place of "vehicle": null. A missing API key returns 401 with {"detail": "API key required. Pass X-API-Key header."}. On the cascade, by contrast, an unknown make, model or other value is not an error: it returns 200 with an empty list.

Tips

  • Cache option lists. Makes, models and years change rarely. Cache the makes list and each make's models for a day (in your backend or CDN) and you will make far fewer requests.
  • Mind concurrency. The limit is 500 requests per minute per account, shared by the cascade and the by-ID endpoint. A dropdown UI makes one request per click, so this is rarely an issue. Jobs that resolve many IDs should pace their requests, reuse what they have already fetched, and back off on 429 (wait for the Retry-After header).
  • Vehicles with no series code. The series list then contains an empty string. Show it as "(no series code)" or "Standard", keep the real value "" in the option, and send series= to continue.
  • Empty list means a mismatched value. Re-send exactly what the previous level returned, including double spaces, and let your HTTP library encode it.
  • Free and Lite plans get 403. Vehicle database endpoints need a Starter plan or above. Check the plan before showing a vehicle picker, or fall back to plate lookup only.