VIN forms look simple: seventeen characters in, structured fields out. The hard part is what you show when the input is wrong, partial, or only half-supported by the API.

Three failure classes

Invalid: fails format or check digit. Do not call the network yet. Explain what failed (length, illegal character, check digit).

Incomplete: user is still typing. Show progress (13/17) without scary errors.

Ambiguous / sparse: API returns 200 with many blank fields. That is not a crash. Say what you know and what you do not.

Soft vs hard fail

Hard fail: block submit for impossible VINs.
Soft fail: allow decode attempts that may return sparse data, but label confidence.

type VinGate = | { ok: true; vin: string } | { ok: false; reason: "length" | "charset" | "checkDigit" }; function gateVin(raw: string): VinGate { const vin = raw.trim().toUpperCase(); if (vin.length !== 17) return { ok: false, reason: "length" }; if (/[IOQ]/.test(vin)) return { ok: false, reason: "charset" }; // check digit omitted for brevity return { ok: true, vin }; } 

Copy that helps buyers

Avoid "Error 500" language. Prefer:

  • "This VIN fails the check digit. Re-type from the dash plate."
  • "We decoded make and model year. Trim and engine came back blank from NHTSA."
  • "A free decode is not a title or salvage report."

Marketplace embed pattern

If you embed VIN checks in listings, never mark a listing "verified" on decode alone. Use decode as a consistency check against the seller's claimed year/make/model.

I maintain VIN Lookup, a free VIN decode based on NHTSA data.