Back to Blog

The VIN Came With a Crash Upsell

September 8, 2026
Share:
The VIN Came With a Crash Upsell

I copied a VIN out of a listing and searched for a decoder. The first page asked for an email. The second wanted a photo of a car it already had the digits for. The third started a crash report I hadn't asked for.

The listing already had the characters. A bumper photo wouldn't have added a thing. I wanted the factory reading. Make, model, year, engine. The United States already publishes that feed. I built the VIN decoder that asks NHTSA and stops there.

I ranked this eighteenth out of 1,754. Score 9.3. About seven hours on the card. Most of that time went into the check digit. I refused to block a VIN for every country over a North American checksum. The year field took time too, because I wouldn't pick the 30-year cycle for you. The cache dies when the process restarts. I wanted that. I didn't want a row that outlives the process and pretends it's still true.

I Wanted the Factory Reading

Usually it's a listing. It can also be a door jamb sticker or a parts invoice. You've got seventeen characters, or you've got most of them, and you want to know what left the plant.

Search for vin decoder free and you get a wall. A box for your email. A crash report you didn't ask for. Then, if you wait long enough, the same NHTSA fields this page shows on the first screen.

A history report is a different product. NHTSA vPIC doesn't have crashes or owners. It doesn't have mileage, a title, a lien, or a market price. If a site promises those from this same free feed, it's padding.

I have the VIN. I do not want the crash upsell.

You paste and read the factory fields. Then you go back to the listing.


Rank 18 of 1,754

I ranked this eighteenth out of 1,754, score 9.3, about seven hours. The card said: paste a 17-character VIN, get make, model, year, engine, and a per-character explainer.

The identifier pile on that list is the same family as the tracking number lookup. You're holding a string. You want one fact back. On the tracking page, that's the carrier. Here it's the factory spec.

Bing put "vin decoder" first on the bare head term vin. The phrase vin decoder itself returned 25 completions, including "vin decoder free" and "vin decoder nhtsa". I didn't buy a volume number behind those. I have the completion count from the ranking pass.

HN Algolia had 65 hits on "vin decoder", including two Show HN launches. cardog-ai/corgi sat at 330 GitHub stars in that same write-up. Those are other people's decoders. I wanted the fields without the upsell. Their star count is their problem.

Reddit is where the gap showed up. A 25-score thread on r/cars said automated decoders generally only work with 17-character VINs, and those weren't standardized until 1981. A mechanic-advice thread answered "do you want to decode a specific VIN" with a link to a third party explainer. I wanted that explainer on the same page as the fields.


Error Code 6 Still Had a Car

NHTSA publishes a sample: 5UXWX7C5*BA, year 2011. I ran that sample in the research sitting for this tool. HTTP 200. Make BMW. Model X3. EngineCylinders 6. DisplacementL 3.0. ErrorCode 6. ErrorText said incomplete VIN. The JSON was 3,994 bytes.

That's one sample. I measured 3,994 bytes that sitting. Yours will differ.

Error code 6 means the VIN is incomplete. The useful fields still came back. A 200 from that feed can still be a partial car. The decoder keeps that as partial, with the incomplete warning on screen. It doesn't stamp "VIN not found" over a BMW X3 that NHTSA already named.

Empty fields stay empty. The page writes "not given by the source". A trim or a gearbox only shows if the feed sent one. The card has no stock photo of that car.

What you pasteWhat NHTSA can doWhat this page shows
Full 17 charactersDecode what the file hasdecoded, or unavailable if the row is empty
Partial with *Fill what the known characters allowpartial, with the incomplete warning kept on screen
Junk, I/O/Q, random punctuationNever reachedinvalid, no upstream call

Seventeen Characters Since 1981

The United States standardized the 17-character VIN in 1981, under 49 CFR 565. Positions 1 to 3 are the world manufacturer identifier. Positions 4 to 8 are the manufacturer's own descriptor. Position 9 is the North American check digit. Position 10 is the year code. Position 11 is the plant. Positions 12 to 17 are the sequence.

That's a map of the 17 positions. The inner codes still belong to the manufacturer. If I don't have their table, the strip won't pretend position 5 always means airbags.

A number from before 1981 may not follow this scheme. Partial mode will still send what you have. The page will say a modern decode may not fit a historic vehicle.


I, O and Q

The letters I, O and Q are left out of a VIN so they aren't read as 1 or 0. 49 CFR 565 is the source for that. I kept that rule.

If you paste an O, this page won't silently turn it into a 0. Same for I and 1. I'd rather show the error than "fix" a character you didn't type.

I strip spaces and hyphens in the middle, and I show the cleaned string before Decode. Control characters and random punctuation get rejected. Full mode is 17 characters and no star. Partial mode is 3 to 17, star allowed, at least three known characters.

NHTSA accepts * for a missing character. The official sample uses one. That's why Paste a sample turns partial mode on and fills 2011.


Position 10 Is a 30-Year Cycle

Position 10 repeats every 30 years. B is 1981 and 2011. The table is in 49 CFR 565.15 Table VII. I, U and Z are unused.

NHTSA recommends sending the model year. The year field on this page is optional. I thought about picking the current cycle when you leave it blank. That would look helpful for one second, and it would be wrong for a 1981 car that uses the same letter. If the letter could be two years, the page asks. You type the year if you know it.

I shipped a check digit from the same part of the rule, Tables III and IV. The statuses are valid, mismatch, not-applicable, and not-checked. A mismatch is a warning. The request still goes through. A check digit scheme that isn't confirmed for that market isn't a reason to reject the VIN worldwide.


Plant Country and Home Country

NHTSA has a plant country field. That's where the vehicle was assembled. The brand's home country is a different fact.

A BMW built in the United States still says United States in that field. I won't swap in Germany to make the card look tidy.

People do that swap in their heads anyway. The logo says BMW, so the car "is German." The plant field is the factory floor. Those can disagree, and when they do I print both.

Manufacturer country, when the feed sends it, sits on its own row. The two labels stay separate on purpose.


What NHTSA Sends Back

The route is POST /api/tools/vin-decode. The body is JSON: a VIN, an optional year, and mode full or partial. The browser doesn't call NHTSA. Softery does, on a Node route that already lives with the rest of the site.

The official path is DecodeVinValues, with format=json, and modelyear only when you typed a year. The route doesn't invent a year. It also ignores a caller URL if someone sends one.

HTTP 200 upstream still has to pass ErrorCode before we call it decoded. We read ErrorCode, including more than one code, and SuggestedVIN if it's there. If NHTSA sends a SuggestedVIN, I show it. You decide whether to use it.

Timeouts and 429 from NHTSA are unavailable. So are 5xx. 400 is our input. 429 is our limiter. 502, 503, and 504 are service problems. The page shows Try again for those. It doesn't write "this VIN does not exist".

Our statusHTTP you seeWhat to do
decoded200Read the card
partial200Read the card and the warning
invalid400Fix the paste
unavailable429, 502, 503, 504Wait, then Try again

The Strip Is a Map

After a result, the VIN is a row of buttons. Click a character and you get the group: WMI, descriptor, check digit, year, plant, sequence.

That's ISO 3779 and 49 CFR 565. A manufacturer's option table is a different book. The note under the strip says what the group is for. A private BMW option code stays unread, because I don't have their book.

Position 10 is the year letter. Positions 1 to 3 are the WMI. That's the public map. Click around if you want to see which slice did what.

Compare another VIN runs one after the other. You decode the second one after the first. A spreadsheet paste doesn't fan out into a dozen upstream calls.


Incomplete On Purpose

There's a button labeled I have an incomplete VIN. Partial mode sits on the page, and the label says what it does. NHTSA will fill what it can. A short old number may not belong to this scheme.

The official sample is the incomplete one on purpose. If the happy path is only a clean 17-character VIN, people with a damaged plate get a shrug. Error code 6 with a BMW X3 is the path I tested first.

Copy spec and the image hide the VIN until you turn that off. Print uses the same card. I thought about leaving the VIN visible on the image by default. That's how a lot of these pages work, and it's how a VIN ends up in a screenshot you didn't mean to share. Local history is off until you tick the box. Clear history wipes it. There's no public list of other people's VINs.


What Leaves This Tab

Decode posts the VIN to Softery. Softery asks NHTSA. A useful full answer can sit in memory on that process for seven days. A useful partial sits for one day. An empty answer sits for ten minutes. Transport errors aren't stored as "this VIN does not exist".

Those times are ours. They aren't NHTSA limits. The cache dies when the process restarts. I didn't write it to Supabase. I didn't put it on a CDN. The response to your browser is private, no-store.

I put the tracking number in the address bar on the other page. I didn't do that here. A VIN identifies a vehicle. A refresh shouldn't turn into a URL you can paste into a chat with the car still in it. The VIN stays out of the address bar. There's no Open Graph page for it either. Mixpanel gets the outcome and the mode, plus a cache hit or miss. The event has no VIN and no hash of one. The NHTSA URL is left off too.

If you want less analytics, block the scripts. The card stays. The VIN already went to Softery and NHTSA when you pressed Decode. I'm not going to pretend the page is local-only.

The limiter is ours too. Ten decodes a minute per client key. Two NHTSA calls at once. One new upstream call a second. A short queue of twenty. Those numbers are starting knobs. NHTSA didn't publish them as a cap, and I'm not selling a load test as capacity. A cache hit still sends the result to the browser. Random VINs skip the cache, so the limiter is the thing that matters.

The upstream URL stays out of the logs. If I run more than one Node process later, that budget has to be split. The first version is one Node process.


Compare Two Readings

A buyer looking at two listings can decode one, then the other, and see which fields match. Same make with a missing trim just means the feed didn't give a trim. I mark the cells that differ. You decode the second VIN after the first one is done.

Two cards side by side help when the listings look like the same car and the price doesn't. A year that matches and a plant that doesn't is a fact you can take back to the seller. A blank trim on both sides is two blanks. That's two missing trims.

There's no car payment calculator on Softery yet. I won't point you at a 404. The catalog is on free tools. The page that sits next to this one is the tracking number lookup, because both jobs start with a string in your hand.


Paste the VIN

  1. Open the VIN decoder.
  2. Paste the VIN, or press Paste a sample for the official incomplete BMW.
  3. Type the year if you know it. Leave it blank if you don't.
  4. Press Decode once. Read the factory card. Open the empty fields if you care.
  5. Copy the spec. Print the card if you want paper. Save the image if that's easier. Keep the VIN hidden unless you need it on the printout.

If NHTSA is slow, press Try again after a moment. Don't hammer it. The limiter is there so a burst of random VINs doesn't turn a public feed into our problem.


Last updated: September 8, 2026 | Reading time: 11 minutes

Written by Evgeniy Poznyak, who would rather read a factory field than a crash upsell.