Price a used car from its VIN, and show the listings behind the number
MarketCheck Price predicts a market price from a VIN, mileage, dealer type and location, and its higher tiers return the comparable listings and the full decode behind it.
MarketCheck5 min read
For developers building appraisal, trade-in, repricing and lending tools
MarketCheck Price answers one question: what is this car worth on a dealer lot near here today? It takes four inputs, the VIN, the mileage, the dealer type (franchise or independent) and a location (a ZIP, or a city with a state), and it comes in three tiers that share those parameters:
- Base,
GET /v2/predict/car/us/marketcheck_price, returns the predicted price and the MSRP. The docs position it for quick estimates and high-volume use. - Premium,
GET /v2/predict/car/us/marketcheck_price/comparables, adds comparable listings with price, mileage and days-on-site statistics, plus a second set the docs call recent comparables. - Premium Plus,
GET /v2/predict/car/us/marketcheck_price/comparables/decode, adds a full NeoVIN decode of the car.
The docs describe the models as trained on recent dealer retail data and the prediction as a reading of current market conditions. They rule out treating it as a forecast, which matters most to lenders.
In short. Send
vin,miles,dealer_typeand a location to the tier you need. Base answers "how much"; the two higher tiers add the evidence, and the top one adds the build as well. Treat the result as one input to an appraisal or a loan decision, with your own adjustments for condition and reconditioning.
What the Premium tier returns
The documented sample prices VIN 2T3DWRFV3LW077677 with miles=76189, dealer_type=independent, zip=80215 and is_certified=true. The Base tier's whole answer is { "marketcheck_price": 29014, "msrp": 38000 }. Here is the Premium answer, trimmed to one listing per set, with dealer names, URLs and most statistics removed:
{
"marketcheck_price": 29014,
"msrp": 38000,
"comparables": {
"num_found": 114,
"listings": [
{ "vin": "2T3DWRFV8LW066769", "year": 2020, "make": "Toyota", "model": "RAV4", "trim": "Limited",
"price": 29101, "miles": 79372, "dos_active": 16, "dist": 3.78 }
],
"stats": {
"price": { "min": 22672, "max": 39995, "median": 29980, "percentiles": { "25.0": 27568.5, "75.0": 32995.25 } },
"dos_active": { "median": 35 }
}
},
"recent_comparables": {
"num_found": 19,
"listings": [
{ "vin": "2T3DWRFV8LW066769", "price": 29499, "miles": 79372, "dos_active": 15 }
],
"stats": { "price": { "median": 30998 } }
}
}Three numbers in this sample look alike and mean different things: the prediction (29,014), the median asking price of the comparables (29,980) and the median of the recent comparables (30,998). The first comes from the model for this VIN and mileage, while the other two describe sets of listings, so show them side by side with their labels. The recent set is also much smaller, 19 listings against 114 in this sample. Note also that the same VIN, 2T3DWRFV8LW066769, appears in both sets at two asking prices, so de-duplicate by VIN before you count or chart comparables.
Appraisals and trade-in estimates
- Customer enters VIN and miles
- Base price at the store's ZIP
- Your condition and recon adjustments
- Appraiser confirms the offer
A trade-in form needs a number fast, and Base is enough: send the store's own dealer_type and ZIP, because the prediction is for a car retailed there. The appraiser's desk wants the evidence, so the same VIN goes to Premium, and the comparables table can list each car's asking price, mileage, days on site and distance (dist) from the store. Sort that table by dist so the nearest cars come first, and adjust for condition and reconditioning in your own code, after the call. Pass is_certified=true only for a car you will certify, since it defaults to false.
The location decides which market the price describes, so pick it per job. For a trade-in, that is the store's ZIP, where the car will be retailed. A lender should use the selling dealer's ZIP and an insurer whatever location its valuation rules name, while a consumer app pricing a private purchase has only the buyer's ZIP to go on. City and state work when no ZIP is known, and the docs require the two together.
A dealer's repricing tool runs the same call per unit on a schedule and places each asking price against the prediction and the comparables' percentiles. A unit priced above the 75th percentile that has also been on site longer than the comparables' median dos_active (35 days in the sample) is a natural first candidate for review, and a manager approves any change.
Unusual use: a lender's LTV sanity check at origination
- Deal packet with VIN, miles and amount
- Base price at the dealer's ZIP
- LTV against your policy
- Comparables for flagged deals
- Credit analyst decides
The docs list loan-to-value among the uses of MarketCheck Price. At origination, divide the amount financed by the predicted price for the car as sold, using the odometer statement's mileage and the selling dealer's type and ZIP, plus certification if the car is certified. Deals under your review line pass, while those above it get a Premium call so the analyst sees the comparables beside the ratio. Your credit policy sets both lines. A 422 means the VIN did not decode, which sends the deal to manual review.
import os
import requests
BASE = "https://api.marketcheck.com/v2/predict/car/us/marketcheck_price"
KEY = os.environ["MARKETCHECK_API_KEY"]
def predict(deal, with_comparables=False):
r = requests.get(BASE + ("/comparables" if with_comparables else ""), timeout=60, params={
"api_key": KEY,
"vin": deal["vin"],
"miles": deal["miles"],
"dealer_type": deal["dealer_type"], # franchise or independent
"zip": deal["dealer_zip"],
"is_certified": "true" if deal["certified"] else "false",
})
if r.status_code == 422:
return None # the VIN did not decode
r.raise_for_status()
return r.json()
def ltv_check(deal, policy):
"""policy holds review_ltv and max_ltv, both set by your credit team."""
base = predict(deal)
if base is None:
return {"decision": "manual", "reason": "VIN did not decode"}
ltv = deal["amount_financed"] / base["marketcheck_price"]
result = {"marketcheck_price": base["marketcheck_price"], "ltv": round(ltv, 3)}
if ltv <= policy["review_ltv"]:
return {**result, "decision": "pass"}
comps = predict(deal, with_comparables=True)["comparables"]
return {
**result,
"decision": "over_policy" if ltv > policy["max_ltv"] else "review",
"comparables_found": comps["num_found"],
"comparable_asking_p25": comps["stats"]["price"]["percentiles"]["25.0"],
"comparable_asking_median": comps["stats"]["price"]["median"],
}The ratio values the collateral on the day of the loan. For how it may hold up over the term, the prediction is the wrong tool, since the docs describe it as current conditions only. Store the response with the loan file as well: the market moves, so a later call for the same car can return a different number, and an auditor will want the one the decision used.
Unusual use: price a car from a photo of its VIN plate
- Photo of the VIN plate
- OCR and check digit
- Premium Plus with decode
- User confirms the build
- Price with comparables
A consumer app or a buyer on an auction lane can skip typing: photograph the VIN plate at the base of the windshield or the door-jamb label, read it with your own OCR, and send the text to Premium Plus. MarketCheck receives the VIN as text and never sees the photo. Two checks catch misreads before anyone sees a price. The check digit in position 9 rejects most single-character errors, and the decode shows the build, so a user can say "that is not my car" when OCR has swapped a character that still passes.
In the documented Premium Plus sample, the decode names a 2020 Toyota RAV4 Limited, version Hybrid Limited AWD, and gives its exterior color, Midnight Black Metallic, a confidence of 0.36. Ask the user about low-confidence attributes like that one. The decode also explains the MSRP: its combined_msrp of 38,000, the figure Base returns as msrp, splits into 36,880 of base MSRP and 1,120 of delivery charges.
const BASE = 'https://api.marketcheck.com/v2/predict/car/us/marketcheck_price/comparables/decode'
// Check digit rules for North American VINs (position 9)
const VALUES: Record<string, number> = {
A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8, J: 1, K: 2, L: 3, M: 4, N: 5,
P: 7, R: 9, S: 2, T: 3, U: 4, V: 5, W: 6, X: 7, Y: 8, Z: 9
}
const WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]
export function validVin(ocrText: string): string | null {
const vin = ocrText.toUpperCase().replace(/[^A-Z0-9]/g, '')
if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(vin)) return null // I, O and Q never appear in a VIN
const sum = [...vin].reduce((total, ch, i) => total + (/\d/.test(ch) ? Number(ch) : VALUES[ch]!) * WEIGHTS[i]!, 0)
const check = sum % 11 === 10 ? 'X' : String(sum % 11)
return vin[8] === check ? vin : null
}
export async function priceFromPlate(ocrText: string, miles: number, zip: string, dealerType: 'franchise' | 'independent') {
const vin = validVin(ocrText)
if (!vin) return { status: 'retake' as const }
const params = new URLSearchParams({
api_key: process.env.MARKETCHECK_API_KEY ?? '', vin, miles: String(miles), zip, dealer_type: dealerType
})
const res = await fetch(`${BASE}?${params}`)
if (res.status === 422) return { status: 'retake' as const } // passed the check digit, failed to decode
if (!res.ok) throw new Error(`MarketCheck Price returned ${res.status}`)
const p = await res.json()
const d = p.decode ?? {}
const asks = p.comparables?.stats?.price?.percentiles
return {
status: 'confirm' as const, // show the build first and the price after the user says yes
build: [d.year, d.make, d.model, d.trim, d.version].filter(Boolean).join(' '),
color: d.exterior_color?.name,
colorConfidence: Number(d.exterior_color?.confidence ?? 0),
price: p.marketcheck_price,
comparables: p.comparables?.num_found ?? 0,
askingRange: asks ? [asks['25.0'], asks['75.0']] : null
}
}The plate does not carry the mileage, so ask for the odometer reading or read it from a second photo. When the user rejects the build, keep the OCR text and the VIN they type instead, since those pairs are the test set for improving your OCR step.
Gotchas
vin,milesanddealer_typeare required, plus eitherzipor bothcityandstate.- A 400 points to missing parameters or an invalid VIN, a 422 to a VIN that did not decode, and a 403 to a tier your key cannot use.
- Money is in USD and mileage in miles, since these endpoints price US cars.
- The prediction is for the dealer type you send. A franchise and an independent store can get different numbers for the same car, so send the type of the store that will retail it.
- Cache by VIN, mileage, dealer type and ZIP for the day, and call again whenever the mileage changes.
- A 429 is either the per-second rate limit or the monthly quota. The
RateLimit-*andQuota-*headers tell them apart, andRetry-Aftersays how long to wait.
Who this is for
Appraisal and trade-in tools get Base for the fast number and Premium for the desk. Dealer repricing tools lean on the comparables and their percentiles. Lenders and credit unions get the LTV check, and insurers valuing a claim, another use the docs list, get the same evidence trail.
Start here
- Start on the Free tier for development. Plans are on the pricing page.
- Run the documented VIN through all three tiers and compare what each adds.
- Build the Base call into your form, then add Premium where a person needs the evidence.
- Keep the MarketCheck Price reference open for the tier details and error codes.