πŸ“– API Documentation

Plain-English guide to the VIN Decoder API. For interactive Swagger, see /docs.

Quick start

Decode any VIN with a single POST request:

curl -X POST https://YOUR-HOST/decode \
  -H "Content-Type: application/json" \
  -d '{"vin": "19XFB4F25DE200650"}'
πŸ’‘ Works with full 17-char VINs, partial VINs (9+ chars), or even just the WMI (3 chars). The decoder always returns whatever it can deduce.

What is decoded

This decoder decodes road vehicles (cars, vans, trucks, buses and motorcycles) and, for the manufacturer codes on its powersports list, ATVs and side-by-sides (also off-road motorcycles registered as ATVs; for a few codes also snowmobiles). It refuses a VIN whose own record names a trailer, a boat or personal watercraft, a tractor (a farm tractor or a semi-truck tractor unit: truck tractors are not decoded yet), lawn or construction equipment, or a forklift, and an off-road vehicle from a code or of a kind that list does not cover.

A powersports vehicle (a VIN whose own record names an ATV class, from a manufacturer code on the powersports list, with a matching check digit) is answered with vehicle_type "powersports" and only the fields measured for that class: the make and the model year read from VIN position 10, each checked against registration records, and the plant country, checked against the vehicles' own NHTSA vPIC records; each only for the manufacturer codes where it passed. Every other field is withheld when the decoder found a value (data_quality reason withheld_powersports_scope) or shows not_decoded when it found none. Polaris (4XA, TAP) and Can-Am (3JB) codes keep their earlier decode with all fields.

Other vehicles are refused with HTTP 422; the message states the evidence (the VIN's own record, or the decoder's table for the manufacturer code), never a conclusion about the vehicle.

Endpoints

POST/decode

Decode a single VIN. Returns make/model/year/engine/transmission/body and per-character breakdown.

Body

{"vin": "19XFB4F25DE200650"}

Response (truncated)

{
  "vin": "19XFB4F25DE200650",
  "make": "HONDA",
  "model": "Civic",
  "year": 2013,
  "engine_code": "LEA2",
  "engine_displacement": 1491,
  "transmission": "CVT",
  "fuel_type": "GASOLINE",
  "body_type": "Sedan",
  "country_rules": {...},
  "positions": [{"pos":1,"char":"1","name":"Country/Region","value":"North America"}, ...],
  "segments": [{"name":"WMI","value":"19X","positions":"1-3"}, ...]
}
POST/decode/batch

Decode up to 50 VINs in one request.

{"vins": ["VIN1", "VIN2", ..., "VIN50"]}
POST/decode/image

Upload a VIN plate photo, get OCR + decode.

curl -X POST https://YOUR-HOST/decode/image \
  -F "file=@vin_photo.jpg"
GET/recalls/{vin}

NHTSA recall campaigns (US-market data) for the vehicle /decode displays: the same list, recall_vehicle and a recall_status (served / none / no_model; 503 = unavailable / not_checked).

GET/image/{vin}

Get a representative Wikipedia thumbnail of the make/model/year. Cached 24h.

GET/validate/{vin}

Validate VIN format + ISO 3779 check digit (no DB hit, instant).

GET/search?make=&model=&year=&limit=

Reverse lookup β€” find WMI+VDS patterns matching a make/model/year.

curl "https://YOUR-HOST/search?make=TOYOTA&model=COROLLA&year=2020&limit=10"
GET/generate?make=&count=

Generate test VINs for development. count up to 50.

GET/health   GET/stats

Service health and dataset coverage metrics.

Versions: decoder_version

Every decode result carries decoder_version (for example "2.22.0"), the release of the decoder that produced it. The same number is in the X-Decoder-Version header of every response and in /health. Store it with any result you save or print: the same VIN can decode differently in a later release.

Releases before August 2026 are Beta 1.0. The What's new page lists every release with its version.

Per-field quality: data_quality

Every decode result includes a data_quality block: the /decode response (full and fast), each result of /decode/batch and the /decode/audit response carry it at the top level; /decode/asean carries it inside standard_decode, /decode/pdf inside each decoded[].decoded, and /decode/image inside decoded. A VIN the decoder refuses (HTTP 422 on /decode, an error entry in a batch) has no decode and so no block. For each of the 21 decoded fields it says whether the value is confirmed for this vehicle, so a result can be printed without reading any internal keys. Expect most fields to be unverified: "verified" is deliberately hard to earn.

Status values

What can make a field "verified"

Only these routes. Each was measured against per-vehicle records (NHTSA vPIC records of other vehicles, registration records) and passed a bar of 16 correct in 17 at 95% confidence (Wilson lower bound).

For every other field this VIN's NHTSA vPIC record is a check, not a confirmation: when it agrees the reason says so (nhtsa_record_agrees); when it differs the field is unverified whatever else says, estimated values included (official_record_disagrees); when it states the field less precisely, more specifically, or spells the same value differently, the reason says which (record_less_precise, record_more_specific, record_spelling_differs). NHTSA records are known to be wrong about the transmission of many hybrids (nhtsa_known_unreliable). Registry lookups matched to a parts catalogue, dealer records, records of other vehicles with the same VIN prefix, catalogues, inferences and engine serial numbers never verify. Nothing is verified when the VIN contains I, O or Q, when its manufacturer code looks mistyped or is unknown, when its check digit fails (North America, China), for a partial VIN, or when the decoder hit an error; warnings then says why.

A fast decode ("fast": true) consults fewer sources and adds the fast_decode warning; its values, and therefore its statuses, can differ from a full decode of the same VIN. Use the full decode when it matters.

Opting out: send "include_quality": false in the body of a /decode or /decode/batch request (it is also honoured by /decode/audit) to skip this block and save about 5.6 KB per result; data_quality is then null and every other key is unchanged. Leaving the option out (or sending true) always includes the block.

Each field carries status, value (always identical to the top-level field of the same name; null when withheld or missing), candidate, source (plain-language origin, or for a verified field what confirms it; never an internal tag), reason_code (stable, machine-readable) and reason (one printable sentence that is true for this decode). summary groups the field names by status. A candidate appears only under these reason codes: withheld_unconfirmed, withheld_weak_source, withheld_record_agrees, withheld_unconfirmed_candidate, withheld_multiple_builds, withheld_multiple_brands, withheld_fuzzy_match.

There is no per-field percentage: the status is the confidence statement. field_meta is kept for backward compatibility; its confidence is a legacy source-tier heuristic, not a probability, and it can disagree with data_quality. Use data_quality.

Example (2010 BMW X5 xDrive35d, truncated)

"data_quality": {
  "schema_version": 1,
  "summary": {
    "verified": [
      "make",
      "year",
      "fuel_type"
    ],
    "unverified": [
      "model",
      "series",
      "submodel",
      "body_type",
      "vehicle_type",
      "engine_displacement",
      "cylinders",
      "drive_type",
      "power_hp",
      "power_kw",
      "doors",
      "seats",
      "plant_country",
      "manufacturer",
      "torque_nm"
    ],
    "estimated": [],
    "withheld": [
      "engine_code"
    ],
    "missing": [
      "transmission",
      "transmission_speeds"
    ]
  },
  "fields": {   // 8 of 21 shown
    "make": {
      "status": "verified",
      "value": "BMW",
      "candidate": null,
      "source": "Manufacturer code (VIN positions 1-3)",
      "reason_code": "wmi_registry",
      "reason": "Fixed by the VIN's first three characters: this manufacturer code was checked against official vehicle records and belongs to this brand only."
    },
    "year": {
      "status": "verified",
      "value": 2010,
      "candidate": null,
      "source": "Model-year code (VIN position 10)",
      "reason_code": "vin_year_code",
      "reason": "Read from VIN position 10 (the model-year code); for this manufacturer code that rule was checked against official vehicle records."
    },
    "fuel_type": {
      "status": "verified",
      "value": "diesel",
      "candidate": null,
      "source": "NHTSA vPIC record (this VIN)",
      "reason_code": "nhtsa_record",
      "reason": "This VIN's own record in the US government's NHTSA vPIC database gives the same value; for this field such records were checked against independent registration records."
    },
    "engine_code": {
      "status": "withheld",
      "value": null,
      "candidate": "M57D30TU2",
      "source": null,
      "reason_code": "withheld_weak_source",
      "reason": "The only source for this value is one the decoder does not serve without confirmation (such as a reference table, a shared VIN prefix or an inference), so it was withheld."
    },
    "engine_displacement": {
      "status": "unverified",
      "value": 2993,
      "candidate": null,
      "source": "Reference table (engines or models)",
      "reason_code": "record_less_precise",
      "reason": "This VIN's own NHTSA vPIC record states this field less precisely than the value shown (for example the engine size only to the nearest 0.1 litre, a power range, a shorter model name or several possible engine codes), so the exact value shown is not confirmed."
    },
    "power_hp": {
      "status": "unverified",
      "value": 265.0,
      "candidate": null,
      "source": "Other vehicles sharing part of this VIN",
      "reason_code": "nhtsa_record_agrees",
      "reason": "This VIN's own NHTSA vPIC record gives the same value, but NHTSA records have not been shown accurate enough for this field (on this kind of vehicle) to count as confirmed."
    },
    "torque_nm": {
      "status": "unverified",
      "value": 520,
      "candidate": null,
      "source": "Engine data (engine not confirmed)",
      "reason_code": "derived_from_withheld_engine",
      "reason": "Taken from engine data, but the decoder withheld this vehicle's engine code (see engine_code), so this figure is not confirmed for this vehicle."
    },
    "plant_country": {
      "status": "unverified",
      "value": "UNITED STATES",
      "candidate": null,
      "source": "Labelled NHTSA vPIC (this VIN)",
      "reason_code": "nhtsa_label_unchecked",
      "reason": "Labelled as taken from this VIN's NHTSA vPIC record, but that record could not be checked for this field here, so it is not confirmed."
    }
  },
  "warnings": []
}

Printing a result (Python)

import requests
r = requests.post("https://YOUR-HOST/decode", json={"vin": "5UXFF0C55ALJ98962"}).json()
dq = r["data_quality"]
for w in dq["warnings"]:
    print("NOTE:", w["message"])
for name, f in dq["fields"].items():
    if f["status"] == "missing":
        continue
    value = f["value"] if f["value"] is not None else "(not shown)"
    line = f"{name:20} {value!s:30} {f['status'].upper()}"
    if f["status"] != "verified":
        line += "  - " + f["reason"]
    if f["candidate"] is not None:
        line += f"  [unconfirmed candidate, not the vehicle's value: {f['candidate']}]"
    print(line)

warnings codes

reason_code values

verified

unverified - what this VIN's own NHTSA vPIC record says

unverified - contradicted inside this decode

unverified - derived from an engine code the decoder did not confirm

unverified - where the value came from

unverified - the whole decode (no field is verified)

estimated

withheld - a candidate MAY be shown

withheld - a candidate is NEVER shown

missing

Progressive decoding

The decoder applies 6 layers of deduction in order:

  1. ISO 3779/3780 rules β€” region, country, year, plant code (works for any VIN)
  2. Exact WMI+VDS lookup β€” VIN patterns (maker code + VIN positions 4-8) built from observed road-vehicle VINs; live count on /stats
  3. NHTSA full-VIN fallback β€” official US decoder
  4. VDS fuzzy prefix match β€” tries 8β†’7β†’6β†’5β†’4 chars
  5. WMI aggregation β€” make/manufacturer from prefix
  6. NHTSA DecodeWMI β€” official manufacturer registry

Even unknown VINs get region/country/year/plant from pure ISO rules.

Country-aware

The decoder reads VINs differently depending on origin. BMW, Toyota, Honda, Tesla, etc. use their own VDS encoding schemes (60+ manufacturer schemes built in). Check digit validation is mandatory only for North American + Chinese VINs.

Censored / partial VINs

Sellers often mask serial digits: JTDKN3EE2D30XXXXX, WBAPH5C55BA2*****, JTD-KN3-DU5-?????. The decoder handles all of these β€” masking characters (X, *, ., ?, #, dashes, spaces) are stripped automatically.