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.

