The build behind the VIN: trim, installed options and window-sticker MSRP
What NeoVIN returns for one VIN, how to turn it into merchandising, window-sticker style pricing and F&I prefill, and how to decode a whole portfolio in batch jobs or catch listings with the wrong trim.
MarketCheck5 min read
For developers at dealers, marketplaces, lenders and F&I providers
A basic VIN decode tells you year, make and model. Most of what buyers and lenders care about sits one level down: the trim, the packages installed at the factory, the colors by their manufacturer names, and what the car stickered for. MarketCheck's NeoVIN decoder, GET https://api.marketcheck.com/v2/decode/car/neovin/{vin}/specs, returns that from a 17-character VIN, and a set of batch endpoints decodes thousands of VINs from one file.
In short. One call returns trim and version, installed options with codes and prices, colors, features and an MSRP breakdown in which
combined_msrpis base MSRP plus delivery plus installed options. Cache each decode by VIN and sum optionsale_pricevalues to reconcile totals. The batch jobs take over when the list runs into the thousands.
What one decode returns
This is the documented sample for VIN 1FTEW1C59LKE56394, trimmed to two of its eighteen installed options and one feature group:
{
"vin": "1FTEW1C59LKE56394",
"squish_vin": "1FTEW1C5_LK",
"year": 2020,
"make": "Ford",
"model": "F-150",
"trim": "XLT",
"trim_confidence": "mc_build_specs|cf_equipments|exact_match",
"version": "XLT SuperCrew 5-1/2 Box",
"drivetrain": "RWD",
"engine": "5.0L V8",
"msrp": 40020,
"delivery_charges": 1695,
"installed_options_msrp": 10085,
"combined_msrp": 51800,
"exterior_color": { "code": "E7", "name": "Velocity Blue Metallic", "base": "Blue", "confidence": "0.74" },
"options_packages": "302A,53A,55B,59S,63T,862,995,XL9,153,17B,50N,655,68X,64S,54R,86B,85P,435",
"installed_options_details": [
{ "code": "302A", "name": "Equipment Group 302A - Luxury", "msrp": "4345", "type": "P",
"confidence": "High", "verified": true, "rule": "Discount", "sale_price": "2595" },
{ "code": "53A", "name": "Trailer Tow Package", "msrp": "995", "type": "P",
"confidence": "High", "verified": true, "rule": "Excludes", "sale_price": "995" }
],
"high_value_features": {
"302A": [
{ "category": "Infotainment", "description": "Satellite Radio" },
{ "category": "Interior", "description": "Heated Seats" }
]
}
}The price fields line up the way the NeoVIN data dictionary defines them. msrp is the base price of that version with no options, and combined_msrp is base MSRP plus delivery_charges plus installed_options_msrp: 40,020 plus 1,695 plus 10,085 is 51,800 in this sample. Features, high-value features and installed equipment are keyed by the option code that brought them, with base equipment under STANDARD, so you always know which package a feature came from.
Two identity fields are worth storing next to the VIN. version is finer than trim (here it adds the cab and bed), and squish_vin keeps the VIN characters that describe the vehicle's pattern (the first eight plus the model year and plant), so cars built to the same basic pattern share it and it works as a grouping key.
Merchandising that matches the build
- New unit in the feed
- NeoVIN decode
- VDP specs, badges and filters
- Merchandiser approves the template
A vehicle detail page needs the packages and features that sell, in terms a shopper can filter on. One decode covers them:
high_value_featuresis the short list of valuable equipment, which suits badges and search filters.featuresandinstalled_equipmentgive the full equipment list for a specs tab, grouped by option code.exterior_colorandinterior_colorcarry the manufacturer's color name and code plus abasecolor for filters.installed_options_detailsnames each package and option with its code, type and price.
Inventory Search filters on the same codes through its options_packages and high_value_features parameters, so the page and your search filters agree about what the car has.
Window-sticker style pricing
To show where the price came from, build the summary from the decode and refuse to print one whose parts do not add up.
- VIN from the DMS
- NeoVIN decode
- Sticker summary that reconciles
- Manager checks before it prints
Label the output as decoded data, because it is a reconstruction of the build, and the car's original window sticker remains the authority.
interface InstalledOption { code: string, name: string, msrp: string, rule: string, sale_price: string }
export async function decode(vin: string) {
const url = new URL(`https://api.marketcheck.com/v2/decode/car/neovin/${vin}/specs`)
url.searchParams.set('api_key', process.env.MARKETCHECK_API_KEY ?? '')
const res = await fetch(url)
if (res.status === 422) return null // invalid or undecodable VIN: fall back to your own data
if (!res.ok) throw new Error(`neovin returned ${res.status}`)
return res.json()
}
export function stickerSummary(d: any) {
const options = (d.installed_options_details ?? []) as InstalledOption[]
// sale_price is what each option adds after package rules; msrp is its standalone price
const optionsTotal = options.reduce((sum, o) => sum + Number(o.sale_price), 0)
return {
title: `${d.year} ${d.make} ${d.model} ${d.trim}`,
version: d.version,
colors: { exterior: d.exterior_color?.name, interior: d.interior_color?.name },
base: d.msrp,
options: options.map(o => ({ code: o.code, name: o.name, price: Number(o.sale_price), list: Number(o.msrp), rule: o.rule })),
destination: d.delivery_charges,
total: d.combined_msrp,
// Hold the summary for review when the parts do not add up
reconciles: optionsTotal === d.installed_options_msrp
&& d.msrp + d.delivery_charges + d.installed_options_msrp === d.combined_msrp
}
}In the F-150 sample, the eighteen options' msrp values add up to 16,640, while their sale_price values add up to 10,085, which is exactly installed_options_msrp. Packages such as 302A carry a Discount rule and a sale_price below their msrp, so summing msrp overstates the options.
F&I menus and lending forms
- Deal created
- NeoVIN decode
- Prefill trim, engine, drivetrain and MSRP
- F&I manager reviews
F&I menus and lender applications both ask for the vehicle's build. Decode once when the deal is created and prefill the form from the response. Store the whole response with the deal, so a later audit can see what the form was filled from; decode_version and updated_at date the decode itself.
Decoding a lender's whole portfolio
- Portfolio VINs from the loan system
- Batch jobs of up to 3,000 VINs
- Decoded builds and a failure queue
- Portfolio analyst reviews failures
A lender that wants trim, options and MSRP on every car it finances has thousands of VINs to decode. The Batch API takes a plain CSV of 100 to 3,000 VINs per job and, once the job finishes, returns a gzip JSONL file with one decode per line. Batch is enabled on your account on request; the docs list it as an Enterprise package offering. Accounts have a limit on active decode jobs, so the script waits for each job to finish before it submits the next:
import csv, gzip, hashlib, io, json, math, os, time
import requests
JOBS = "https://api.marketcheck.com/v2/batch/neovin/decode/jobs"
KEY = {"api_key": os.environ["MARKETCHECK_API_KEY"]}
def submit(vins, reference):
body = io.StringIO()
csv.writer(body).writerows([["vin"]] + [[v] for v in vins]) # a vin header, plain CSV
r = requests.post(JOBS, params=KEY, timeout=120,
headers={"Idempotency-Key": reference}, # a retried submit returns the same job
files={"file": ("vins.csv", body.getvalue(), "text/csv")},
data={"client_reference": reference})
r.raise_for_status()
return r.json()["job_id"]
def wait(job_id):
while True:
job = requests.get(f"{JOBS}/{job_id}", params=KEY, timeout=60).json()
if job["status"] != "PROCESSING":
return job
time.sleep(60) # the documented polling interval; a webhook_url avoids polling
def download(job_id):
r = requests.post(f"{JOBS}/{job_id}/download", params=KEY, timeout=300) # 1 of 3 downloads
r.raise_for_status()
if hashlib.sha256(r.content).hexdigest() != r.headers["X-File-Checksum"].lower():
raise ValueError("checksum mismatch")
with open(f"{job_id}.jsonl.gz", "wb") as f: # results expire 30 days after the job ends
f.write(r.content)
return [json.loads(line) for line in gzip.decompress(r.content).splitlines()]
def decode_portfolio(vins, prefix):
if len(vins) < 100:
raise ValueError("under 100 VINs, use the single-VIN endpoint")
size = math.ceil(len(vins) / math.ceil(len(vins) / 3000)) # even jobs, none under 100
decoded, failed = [], []
for n, start in enumerate(range(0, len(vins), size)):
job = wait(submit(vins[start:start + size], f"{prefix}-{n}")) # change prefix to resubmit
if job["status"] == "FAILED":
raise RuntimeError(f'{job["error_code"]}: {job["error_message"]}')
for row in download(job["job_id"]):
(decoded if row["http_status_code"] == 200 else failed).append(row)
return decoded, failedEach output line carries its own http_status_code: 200 for a decode, 400 for an invalid VIN, 422 for one that could not be decoded (with a reason), and 500 for a processing error. The failure list is the portfolio team's queue for VINs that were keyed wrong in the loan system. The decoded rows let the team segment the book by trim, powertrain, drivetrain and installed packages, and give every car the same reference for its original sticker price, combined_msrp, independent of the figures on each contract. If you register a webhook_url with a webhook_secret, verify the X-Webhook-Signature header (HMAC-SHA256 over the timestamp and the raw body) and reject anything older than five minutes.
Catching mis-trimmed listings
Marketplaces and dealer groups can check the trim a dealer typed against the decode.
- Listings with the trim the dealer typed
- NeoVIN decode per VIN
- Compare trim and claimed features
- Merchandiser fixes or confirms
The check runs in four steps:
- Decode each VIN once and cache it; a VIN's factory build does not change.
- Compare the listing's trim with
trimafter normalizing case and spacing. When they differ, compare it withversiontoo, because a listing may carry the version string as its trim. - Check claimed equipment against
high_value_featuresandfeatures. A listing that advertises heated seats on a build without them is worth a second look. - Send each disagreement to a person with both values and the
trim_confidencestring, which records how the decoder chose the trim.
When a listing claims a package the decode did not find, GET /v2/decode/car/neovin/{vin}/options-packages lists every package that was available for that VIN when it was sold, with codes, names and MSRP. That narrows the review to packages that could have been fitted, which a person can then confirm on the car or its original sticker.
Limits to design around
- 422 on the single endpoint means an invalid VIN or a failed decode. With
include_generic=true, generic specifications come back as a fallback when a full decode is not possible. - Full VINs only. The decoder does not accept squish VINs.
- Installed versus available.
include_available_options=trueadds packages that could have been ordered; keep them out of the installed list. - Mixed confidence formats. Color confidence is a number in a string, option confidence reads
HighorLow,verifiedcan benull, andtrim_confidencedescribes a method. - Batch files. Plain CSV with a
vinheader (gzip is rejected), 10 MB at most, and 100 to 3,000 VINs. A file outside that range is accepted with a 202 and fails during processing. - Batch jobs. Submitting past your limit on active jobs returns 409
active_job_exists. Poll no faster than every 60 seconds, and save the file on the first download: each job allows three, and results are kept 30 days after the job ends. - 429s. Honor
Retry-After, and readQuota-*headers to tell the monthly quota from the per-second limit.
Who this is for
Developers at dealer groups and marketplaces building VDP content and listing checks, and at lenders and F&I providers who need the exact build behind every deal and every loan on the book.
Start here
- Create a free account and copy your API key. Start on the Free tier for development; current plans are on the pricing page.
- Decode a VIN from your own lot and compare the result with its window sticker.
- Run
stickerSummaryon a few decodes and check that each one reconciles. - Read the NeoVIN Decoder docs, including the batch section, before planning a portfolio run.