Tutorials

Decode a VIN with an API call

A vehicle identification number packs the manufacturer, the vehicle type, the model year and the plant into 17 characters. This tutorial walks through what each position means, how the ninth character is calculated, and how to decode a VIN with one request to the VIN decoder. You will end with a short function you can drop into a listing form or an intake script.

By LookValid engineering. Published 2026-10-19.

How a VIN is laid out

US federal rules set the content of a VIN in 49 CFR 565.15 1. It has four sections: three characters, then five, then one, then eight. This post calls the first three the WMI (world manufacturer identifier), positions 4 to 8 the VDS (vehicle descriptor section) and positions 10 to 17 the VIS (vehicle identifier section). Those labels are shorthand, and the regulation itself speaks of sections 1.

VIN positions as set out in 49 CFR 565.15
PositionsShorthandWhat they carry
1 to 3WMIIdentifies the manufacturer
4 to 8VDSDescribes the vehicle; the attributes encoded depend on the vehicle type
9Check digitA digit 0 to 9 or the letter X, worked out from the other 16 characters
10VISModel year
11VISPlant of manufacture
12 to 17VISProduction sequence number for high-volume manufacturers

VIN positions as set out in 49 CFR 565.15

The same section assigns a value to every letter except I, O and Q, and the model year and plant characters come from the same character set 1. The decoder follows that: it rejects any VIN that is not exactly 17 characters or that contains I, O or Q, before it looks anything up.

The check digit, worked through

The regulation turns the check digit into arithmetic: each character gets a value (digits keep their face value, letters map through a table), each position has a weight, the products are added and the sum is divided by 11 1. The remainder gives the ninth character, and a remainder of 10 is written as X 1.

Here is the code the decoder runs, in JavaScript. Position 9 has weight 0 because it is the character being checked.

const TRANSLITERATION = {
  A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8, J: 1, K: 2, L: 3, M: 4, N: 5,
  P: 7, R: 9, S: 2, T: 3, U: 4, V: 5, W: 6, X: 7, Y: 8, Z: 9,
};
const WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
​
export function checkDigit(vin) {
  let sum = 0;
  for (let i = 0; i < 17; i++) {
    const c = vin[i];
    sum += (c >= '0' && c <= '9' ? Number(c) : TRANSLITERATION[c]) * WEIGHTS[i];
  }
  const r = sum % 11;
  return r === 10 ? 'X' : String(r);
}
​
console.log(checkDigit('1HGCM82633A004352')); // 3, the ninth character

Check digit calculation

For the VIN 1HGCM82633A004352, the products by position are 8, 56, 42, 15, 16, 24, 4, 60, 0, 27, 8, 0, 0, 20, 12, 15 and 4. They add up to 311. Dividing 311 by 11 leaves a remainder of 3, and the ninth character of the VIN is 3, so the check passes. Change any one character and the sum moves, so most single typing mistakes fail the check.

Why a valid VIN can fail the check

The check digit rule belongs to the US regulation 1, and the vehicle data behind the decoder covers vehicles intended for sale or importation into the United States 3. A VIN on a vehicle built for another market may not follow the US rule. For that reason the VIN decoder verifies the check digit by default and refuses a mismatch with a message that tells you the digit it expected. If you know the VIN is real and was built outside North America, send ignore_check_digit: true and the length and character rules still apply.

The data source also says that vehicles not intended for sale or importation into the US return a limited result 3. Expect more empty fields for those.

Decode a VIN with curl

The decoder checks the VIN locally first, then asks the NHTSA vPIC service, which is the US government vehicle decoder 2. VINs that fail the local checks are refused with HTTP 400 and cost nothing, so a typo never reaches NHTSA. Try one VIN with curl before you write code.

curl -X POST https://YOUR_API_HOST/v1/vin-decoder \
  -H "Authorization: Bearer $LOOKVALID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"vin":"1HGCM82633A004352"}'

Decode one VIN

The response for that VIN has found: true and a data object. These are the fields, taken from the live capture for that VIN:

Fields in data
FieldValue for the sample VINNotes
model_year, make, model, trim2003, HONDA, Accord, EX-V6Top-level strings, or null when NHTSA has no value
body_class, doors, transmissionCoupe, 2, Automaticdrive_type was null for this VIN
enginecylinders 6, displacement_l 3, horsepower 240Also model, configuration, kw, turbo and manufacturer
plantMARYSVILLE, OHIO, UNITED STATES (USA)city, state, country and company
manufacturer, gvwrAMERICAN HONDA MOTOR CO., INC.; Class 1CGVWR comes back as a text class with pound and kilogram ranges
safetyairbags, seat_belts, driver_assistanceMostly null for a 2003 vehicle
decode_qualitycompletecomplete or partial
warnings, additional_notes, suggested_vinempty list, null, nullFilled when NHTSA reports a problem with the VIN

Fields in data

A null value means NHTSA has no data for that field. It does not mean the vehicle lacks the feature, and the response says so in its notes field. A decode_quality of partial means NHTSA could not resolve every part of the VIN, so check warnings before you trust the model and trim.

Call it from code

This function returns a small summary and turns every refusal into an error that carries the API's message. It uses the built-in fetch, so it has no dependencies.

const API = 'https://YOUR_API_HOST';
​
export async function decodeVin(vin) {
  const response = await fetch(`${API}/v1/vin-decoder`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.LOOKVALID_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ vin }),
  });
  const body = await response.json();
  if (!response.ok) {
    // A bad length, a bad character or a wrong check digit is a 400 with a message that names the problem.
    throw new Error(body.error?.message ?? `HTTP ${response.status}`);
  }
  if (!body.found) return { vin, decoded: false, reason: body.reason };
  const { model_year, make, model, trim, decode_quality } = body.data;
  return { vin, decoded: true, model_year, make, model, trim, decode_quality };
}
​
console.log(await decodeVin('1HGCM82633A004352'));

decode-vin.js

  • Input: vin is required. model_year (1981 to 2040) is optional, and NHTSA recommends always sending it when you have it 2. ignore_check_digit is optional.
  • Cost: a VIN that decodes costs 1 credit. A refused VIN and a VIN with no data are free.
  • Repeats: the same VIN, model year and check setting is served from your account's cache for 7 days at no charge.
  • Spelling: the VIN is trimmed and upper-cased before it is checked, so lower-case input from a form is fine.

To try a VIN without writing code, use the free VIN decoder. To see how keys work, start at the documentation.

Use the result in a form

  1. Ask for the VIN and, if you have it, the model year.
  2. Call the decoder on submit. On a 400, show the message beside the field, because it names the exact problem.
  3. On found: false, keep the VIN and let the person continue, since a real VIN can be missing from the source.
  4. On a match, prefill make, model and year, and let the person correct them.

Treat the decoded fields as a prefill, not a ruling. They come from what the manufacturer reported to NHTSA, and plenty of fields on older vehicles will be empty.

Sources

  1. US eCFR: 49 CFR 565.15, VIN content requirements, read 2026-10-19
  2. NHTSA vPIC: Vehicle API documentation, read 2026-10-19
  3. NHTSA vPIC: Product Information Catalog and Vehicle Listing, read 2026-10-19