DEV Community

Vin Lookup
Vin Lookup

Posted on

What NHTSA vPIC Will Not Decode: Blank Fields, Incomplete Specs, and How to Handle Them

NHTSA's vPIC decode is the default free source for U.S.-oriented VIN structure: you send a 17-character VIN, you get a flat row of manufacturer attributes. What newcomers miss is that a successful HTTP 200 is not the same as a complete vehicle card. Many fields arrive empty. Some Error Codes mean "we recognized the VIN pattern but cannot fill this attribute." Treating blanks as bugs - or inventing values to fill them - is how free tools lose trust.

This post covers why blanks happen, which fields are often incomplete, and a TypeScript pattern for honest empty-state UX.

A 200 response is not a full spec sheet

DecodeVinValuesExtended returns JSON with a Results array. For a typical call you read Results[0]. That object can contain Make, Model, ModelYear, BodyClass, EngineCylinders, PlantCity, ErrorCode, ErrorText, and dozens of other keys.

Important distinctions:

  • Transport success means the API answered. It does not mean every marketing attribute exists for that VIN.
  • Partial decode is normal. A WMI and year may resolve while trim, drive type, or fuel type stay blank.
  • ErrorCode / ErrorText often travel with the same row. You can get useful Make/Model alongside codes that say some variables did not decode.

If your UI only checks res.ok and then renders every key as if it were present, users will see a wall of empty cells or, worse, your fallback guesses.

Why fields come back empty

Common, legitimate reasons:

  1. Manufacturer filing gaps. vPIC reflects what manufacturers filed for that WMI / model year structure. Not every optional attribute is populated for every year.
  2. Ambiguous VDS. Positions 4-8 do not always map one-to-one to a unique trim or options package in the public tables.
  3. Older or niche vehicles. Classic, gray-market, incomplete, or low-volume filings may decode thinly.
  4. Wrong expectation of "spec." Buyers want horsepower, exact option packages, and MSRP. Those are not guaranteed VIN-structure fields in a free public decode.
  5. Coverage limits. Strength is highest for vehicles relevant to the NHTSA vPIC dataset. Do not promise global completeness.

Product rule: empty means "unknown from this source," not "this car has no engine" and not "the API is broken."

Fields that often stay incomplete

Area Examples UX note
Plant geography PlantCity, PlantState, PlantCountry Show only when present; never invent from plant letter alone
Powertrain detail Displacement, cylinders, fuel Prefer "not in decode" over guessed catalogs
Body / doors BodyClass, Doors Useful when filled; optional when blank
Series / trim Series, Trim, Trim2 Highest overclaim risk if you invent them

Make, Model, and ModelYear are the usual spine. Even those can fail for bad input - which is why check-digit validation before the fetch still matters.

TypeScript: normalize blanks before you render

Do not pass raw vPIC strings straight into the UI. Normalize once:

type VinAttr = {
  key: string;
  label: string;
  value: string | null;
};

const INTERESTING = [
  ["Make", "Make"],
  ["Model", "Model"],
  ["ModelYear", "Model year"],
  ["BodyClass", "Body"],
  ["DriveType", "Drive"],
  ["FuelTypePrimary", "Fuel"],
  ["PlantCity", "Plant city"],
  ["PlantCountry", "Plant country"],
  ["ErrorText", "Decode notes"],
] as const;

function clean(raw: unknown): string | null {
  if (raw == null) return null;
  const s = String(raw).trim();
  if (!s || s === "0" || /^not\s*applicable$/i.test(s)) return null;
  return s;
}

export function attrsFromVpicRow(row: Record<string, unknown>): VinAttr[] {
  return INTERESTING.map(([key, label]) => ({
    key,
    label,
    value: clean(row[key]),
  }));
}

export function splitPresent(attrs: VinAttr[]) {
  return {
    known: attrs.filter((a) => a.value && a.key !== "ErrorText"),
    missing: attrs.filter((a) => !a.value && a.key !== "ErrorText"),
    notes: attrs.find((a) => a.key === "ErrorText")?.value ?? null,
  };
}
Enter fullscreen mode Exit fullscreen mode

Render known in a primary table, missing as a short "Not returned by decode" list (or omit), and notes in a muted callout. That layout teaches users that incompleteness is data quality, not a crash.

Honest UX patterns

1. Separate identity from options. Put Make / Model / Year in a hero block. Put optional specs in a secondary section labeled "Additional attributes (when available)."

2. Never invent trim. If Series and Trim are blank, do not scrape a marketing site to "complete" the card. Partial truth beats confident fiction.

3. Surface ErrorText once. Show decode notes near the results, not buried in a raw JSON expand.

4. Empty is not green. Do not show a "clear" badge because optional fields are blank. Blank plant city is not "no flood damage."

5. Disclose scope on the results page. One short line is enough: manufacturer attributes from public decode data; not a title, accident, or lien history.

6. Offer next steps that match the gap. If the buyer needs salvage brands or odometer trail, point them to a history report and a pre-purchase inspection - not to another free decode of the same VIN.

Minimal client flow

const VIN_RE = /^[A-HJ-NPR-Z0-9]{17}$/;

export async function decodeForUi(raw: string) {
  const vin = raw.trim().toUpperCase();
  if (!VIN_RE.test(vin)) {
    return { ok: false as const, reason: "Invalid VIN format" };
  }

  const res = await fetch(
    `https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValuesExtended/${vin}?format=json`
  );
  if (!res.ok) return { ok: false as const, reason: `vPIC HTTP ${res.status}` };

  const row = ((await res.json()).Results?.[0] ?? {}) as Record<string, unknown>;
  const parts = splitPresent(attrsFromVpicRow(row));

  if (!parts.known.some((a) => a.key === "Make" || a.key === "ModelYear")) {
    return {
      ok: false as const,
      reason: parts.notes ?? "VIN did not decode enough identity fields",
    };
  }

  return { ok: true as const, vin, ...parts };
}
Enter fullscreen mode Exit fullscreen mode

Cache carefully, respect rate limits, and do not log full VIN lists longer than you need.

Disclosure

I maintain VIN Lookup, a free manufacturer decode built around public NHTSA-style data. The product goal is clear attributes and clear gaps: show what the decode returns, leave blanks blank, and stay explicit that title history and inspections live outside this layer.

Takeaways

  • vPIC blanks are expected; design for them.
  • Normalize empty strings before render.
  • Put ErrorText where humans will read it.
  • Never fill trim or history from imagination.
  • One honest scope line beats a decorative "complete report" label.

If you have shipped a decode UI, the interesting discussion is not how many columns you show - it is how you behave when half of them are empty.

Top comments (0)