Stock what turns: market days supply and sales stats for any trim and ZIP
Two small endpoints tell you how long today's supply of a trim will last near a ZIP and how fast similar cars sold, with builds for dealers, OEM regional teams and marketplaces.
MarketCheck5 min read
For developers at dealer groups, OEMs and marketplaces
Every stocking decision comes down to one question: if I buy this car, how long will it take to sell here? MarketCheck answers it with two endpoints that are small to call and easy to cache.
- Market Days Supply,
GET https://api.marketcheck.com/v2/mds/car, divides the active inventory that matches your filters by the average daily sales over the past 45 days. The result is how many days today's supply would last at the current pace, so a lower number means the car turns faster. - Inferred Sales Stats,
GET https://api.marketcheck.com/v2/sales/car, summarizes the sales MarketCheck inferred for a make, model, year or trim over a 90-day window: how many sold, and the spread of days on market, last listed price and miles.
Both sit on MarketCheck's listing data. Sales are inferred from listing activity, and prices are the last listed (asking) prices, so treat the numbers as market signals rather than a registration count.
In short. MDS tells you how tight supply is right now, and Sales Stats tells you how similar cars sold over the last 90 days. Call MDS per year, make, model and trim (or per VIN) around a ZIP and cache it for the day. Check the counts behind every ratio before acting on it.
What the two responses look like
MDS returns three numbers. This is the documented sample for VIN 5FNRL6H73PB057520 with exact=true and debug=true, which also echoes the year, make, model and trim the VIN matched:
{
"mds": 59,
"total_active_cars_for_ymmt": 329,
"total_cars_sold_in_last_45_days": 252,
"debug": [{ "year": [2023], "make": ["Honda"], "model": ["Odyssey"], "trim": ["SPORT"] }]
}The arithmetic is visible: 252 sold over 45 days is 5.6 a day, and 329 active cars divided by 5.6 is about 59 days.
Sales Stats returns counts plus a statistics block per measure. Here is the documented trim-level sample for ymmt=2023|Toyota|Camry|LE with car_type=used, trimmed to the fields discussed below:
{
"count": 3050,
"cpo": 647,
"non_cpo": 2403,
"inventory_type": "used",
"dom_stats": { "median": 57, "mean": 79, "trimmed_mean": 67, "iqr": 61 },
"price_stats": { "median": 23559, "mean": 23700, "iqr": 2998 },
"cpo_dom_stats": { "median": 47 },
"cpo_price_stats": { "median": 24916 }
}For used cars, the plain *_stats blocks describe the non-certified sales and the cpo_*_stats blocks the certified ones. The CPO blocks appear only when certified cars are in the set.
Pick the market before the number
The two endpoints do not share a geography. MDS accepts the Inventory Search location filters (zip with radius, latitude and longitude, city, state and msa_code) plus dealer filters such as dealer_id and source. Sales Stats has three levels: national by default, state, and city_state for one city. A store-level view therefore pairs a local MDS with state-level sales, as the code below does. Keep the MDS radius close to where your buyers shop, since widening it to find more sales also mixes in markets that behave differently. Sales Stats has no radius or metro filter, so the nearest thing to a metro view there is the metro's main city through city_state.
A stocking list for one store
- Weekly
- Candidate trims from your buyers
- Days supply around the store
- 90-day sales stats for the state
- Your volume floor and ranking
- Buyer builds the list
The buyer supplies the candidates (from your own sales history or an auction run list), and the code ranks them by how fast they turn around the store:
import os
import requests
BASE = "https://api.marketcheck.com/v2"
KEY = os.environ["MARKETCHECK_API_KEY"]
def get(path, **params):
r = requests.get(BASE + path, params={"api_key": KEY, **params}, timeout=60)
r.raise_for_status()
return r.json()
def supply(ymmt, store):
res = get("/mds/car", ymmt=ymmt, car_type="used", zip=store["zip"], radius=store["radius"])
return {"mds": res["mds"], # None when nothing sold in the last 45 days
"active": res["total_active_cars_for_ymmt"],
"sold_45d": res["total_cars_sold_in_last_45_days"]}
def sales(ymmt, store):
# Regenerated monthly over a 90-day window, so cache this for the month
res = get("/sales/car", ymmt=ymmt, car_type="used", state=store["state"])
return {"sold_90d": res["count"], "type": res["inventory_type"],
"median_dom": res.get("dom_stats", {}).get("median"),
"median_price": res.get("price_stats", {}).get("median")} # last listed, not transaction
def stocking_list(candidates, store, min_sold):
rows = []
for ymmt in candidates:
row = {"ymmt": ymmt, **supply(ymmt, store), **sales(ymmt, store)}
if row["sold_45d"] >= min_sold: # the store's own floor for a signal worth acting on
rows.append(row)
# Fastest turn first; a trim with no recent sales (mds is None) sorts last
return sorted(rows, key=lambda r: (r["mds"] is None, r["mds"] or 0))
store = {"zip": "80215", "radius": 50, "state": "CO"}
ranked = stocking_list(["2023|Toyota|Camry|LE", "2023|Honda|Odyssey|Sport"], store, min_sold=20)MDS answers "how tight is supply here today", and the sales stats add "how fast and at what last listed price did similar cars go". A trim with a low MDS and a short median days on market is the easy buy; a low MDS resting on a thin sold count is a question for the buyer, which is why the floor lives in your code where they can change it.
A turn check on a trade-in or an aging unit
- Trade-in or aged unit VIN
- Days supply for that exact build
- Sales stats for similar VINs
- Retail or wholesale rule
- Manager decides
For one car, pass its VIN instead of a YMMT. MDS takes vin with exact=true to match year, make, model, trim, version and build code, plus zip, radius and car_type=used. Sales Stats takes the same vin and converts it to a taxonomy VIN for similar cars. Add debug=true to the MDS call the first time you see a VIN so you can confirm which YMMT it matched before the number reaches a manager.
A workable rule sets both numbers against your own history. If days supply for that exact build is longer than your lot usually needs to retail a car, and similar cars' median days on market is past your aging policy, the manager sees a wholesale suggestion with the evidence attached. The thresholds come from your own sales records.
Allocation signals for an OEM regional team
The same endpoint gives an OEM regional team a weekly read on where each trim is tight or piling up.
- Weekly
- Trims and regions to watch
- New-car days supply by state and trim
- Sold VINs for the fast movers
- Allocation model
- Regional manager decides
A regional team can run the same call with car_type=new, one make, model and trim at a time, per state. Comparing states for the same trim shows where supply is thin against the recent sales pace and where it is piling up. Filtering by dealer_id, source or dealership_group_name gives the same measure for a single dealer or group, which is the docs' own turnover example.
Add include_sold=true to get the list of VINs sold in the 45-day window (up to 10,000), then decode a sample of them to see which colors and packages moved. Pair it with /v2/sales/car and car_type=new per state for 90-day volume. These signals come from what dealers listed and what left their lots, so they sit beside the order and allocation data the team already holds.
Ranking marketplace listings by days supply
A marketplace can sort or badge listings by how quickly their kind of car turns locally.
- Listings on a results page
- Group by YMMT and market
- One MDS call per group per day
- Sort or badge by demand
- Merchandising reviews the rule
Calling MDS per listing would be wasteful, since every listing of the same year, make, model and trim in one market gets the same answer. Group the listings first and make one call per group per day:
const MDS = 'https://api.marketcheck.com/v2/mds/car'
// Replace with your shared cache in production; one entry per YMMT and market, refreshed daily
const cache = new Map<string, { day: string, mds: number | null }>()
async function daysSupply(ymmt: string, zip: string, radius: number): Promise<number | null> {
const day = new Date().toISOString().slice(0, 10)
const key = `${ymmt}|${zip}|${radius}`
const hit = cache.get(key)
if (hit?.day === day) return hit.mds
const url = new URL(MDS)
const params = { api_key: process.env.MARKETCHECK_API_KEY ?? '', ymmt, zip, radius: String(radius), car_type: 'used' }
for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value)
const res = await fetch(url)
if (!res.ok) throw new Error(`mds returned ${res.status}`)
const { mds } = await res.json()
cache.set(key, { day, mds })
return mds
}
// Tightest supply first; null (nothing sold in 45 days) sorts last
export async function rankByDaysSupply<T extends { ymmt: string }>(listings: T[], zip: string, radius: number) {
const supply = new Map<string, number | null>()
for (const ymmt of new Set(listings.map(l => l.ymmt))) supply.set(ymmt, await daysSupply(ymmt, zip, radius))
return [...listings].sort((a, b) => (supply.get(a.ymmt) ?? Infinity) - (supply.get(b.ymmt) ?? Infinity))
}The groups are sequential on purpose: a results page with many trims should not burst past the per-second rate limit.
Limits to design around
mdscan be null. It isnullwhen nothing matching sold in the past 45 days. Show "no recent sales" in its place.- Thin markets. The documented local example (used 2022 and 2023 Camry LE within 50 miles of ZIP 78701) returned an MDS of 45 from 2 active cars and 2 sales. Read
total_active_cars_for_ymmtandtotal_cars_sold_in_last_45_daysbefore trusting the ratio. vin, notvins. MDS finds similar cars throughvin, andexact=truehas no effect without it. Withoutexact=true, a VIN matches only up to the model level, so passexact=trueor an explicitymmtwhen you mean one trim.- A monthly window. Sales Stats is regenerated monthly over the 90 days that end 7 days before the refresh. Cache it for the month.
- Say which inventory type. The Sales Stats defaults section says used, yet the documented make-level sample, sent without
car_type, came back withinventory_type: new. Passcar_typeevery time and checkinventory_typein the response. - One geography.
city_state(such asjacksonville|FL) takes precedence overstate, so send one or the other. - Pipes in values.
ymmt,mm,ymmandcity_stateare pipe-separated; let your HTTP client encode them. - Canada. Both endpoints take
country=ca. - 429s.
RateLimit-*headers describe the per-second limit andQuota-*headers the monthly quota. HonorRetry-After.
Who this is for
Developers building stocking, appraisal and allocation tools for dealer groups and OEM regional teams, and search and merchandising features for marketplaces.
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.
- Call MDS for a trim you stock at your store's ZIP, and compare it with how long that trim sat on your lot.
- Add Sales Stats for the same trim and state.
- Read the Market Days Supply and Inferred Sales Stats docs.