Where else is this car advertised, and at what price?
One Inventory Search call returns every active listing of a VIN across the sites that carry it, and a short wrapper compares their prices and fee fields and hands a person the verdict.
MarketCheck6 min read
For compliance leads and developers at dealer groups, retailers and dealer-website vendors
A car in a dealer group's stock can be listed several times at once: on its store's website and the group's, and on every other site the group syndicates its inventory to. Each copy shows its own price, and nothing forces them to agree.
The FTC's price-transparency FAQs for auto dealers, published Sep 15, 2026, apply to every one of those sites. The smallest useful check takes one VIN and returns every active copy of that car with its price.
In short. Call Inventory Search with
vinandnodedup=true, and every active listing of the car comes back, each with itssource,price, dealer block, capture dates and, when the page lists them, fee fields. A short wrapper turns that into a row per copy and a verdict for a person to check.
Why one car shows more than one price
Syndication makes the copies: the store's inventory system feeds every site that carries its stock, and each shows what it last received. A price change can reach those sites at different times, and MarketCheck crawls each dealer website once a day, so two copies can be captured hours apart. Pages can also disagree about what the number includes: one can put its fees in the price where another leaves them out. And one site can lead with a price after a conditional discount, such as dealer financing, where another shows the price any buyer pays.
What the FTC FAQs say
The FAQs are FTC staff guidance, and four of their points matter here:
- The price. The advertised price must be "the actual price any consumer can walk in and pay", and a fee the dealer requires "must be included in the advertised price". Only charges a government agency requires the consumer to pay directly may stay out (FAQ 2).
- Every website. Dealership and third-party websites are among the channels named, and "every one of these touchpoints is subject to the FTC Act" (FAQ 3). On any webpage that states an amount a consumer may pay, including inventory-search and vehicle-listing pages, the actual price must be the most prominent amount (FAQ 4).
- Everyone with control. Responsibility sits with "everyone who has control over the advertising", and dealers should give third parties the actual price and take all steps within their control to have it shown most prominently (FAQ 12).
- Nothing new. Price transparency "is not a new requirement" (FAQ 13).
The FAQs are staff views, not binding on the public or the Commission. A car advertised at two prices at once tells shoppers two different things about what it costs; the check finds those cars, and your compliance team decides which copy is right. This is not legal advice; confirm with counsel how the FAQs and your state's rules apply to you.
One call returns every copy
Since Aug 31, 2026 the Inventory Search docs have described this as an FTC cross-channel price consistency check: search by vin with nodedup=true, and the response holds the listing on the dealer's own website plus the copies on other websites its inventory is syndicated to, such as dealership group sites. The documented request asks for up to 50 copies, highest price first:
curl "https://api.marketcheck.com/v2/search/car/active?api_key=YOUR_API_KEY&vin=1FTFW1E81PKE39204&nodedup=true&rows=50&sort_by=price&sort_order=desc"Without the flag, the API returns one listing per VIN, the copy MarketCheck attributes to the dealer that has the car. It also has to be a search by VIN: a dealer parameter such as source or dealer_id switches Inventory Search to counts and stats with rows=0. The VIN list comes from your own inventory system, and a filter on source or dealer in code keeps only your group's sites.
| Field | What it tells you |
|---|---|
source, vdp_url |
The site, and the page to open |
price |
The advertised price, absent when the page shows none |
msrp |
The MSRP as the dealer's page shows it |
fees, price_includes_fees |
Total fees identified on the page, and whether the price includes them (documented Sep 15, 2026) |
dealer, mc_dealership |
The dealer, its website and its group |
first_seen_at_date, last_seen_at_date |
When this listing began (a new price starts one) and when it was last seen unchanged |
ref_price |
The previous price at the same site |
vehicle_status, seller_type |
A status such as Coming Soon, and always dealer here |
The wrapper: one VIN in, a table and a verdict out
- Compliance lead enters a VIN
- Every active copy of the VIN
- One row per copy, one verdict
- Lead checks each page
When a shopper says the car was cheaper online, the compliance lead enters the VIN and gets a row per copy, with the verdicts on top:
const SEARCH = 'https://api.marketcheck.com/v2/search/car/active'
// The fields the check reads. A field with no value is left out of the listing.
interface Copy {
source: string
vdp_url?: string
price?: number
msrp?: number
fees?: number // only when fees were identified on the page
price_includes_fees?: boolean
vehicle_status?: string
first_seen_at_date: string
last_seen_at_date: string
dealer?: { name: string }
mc_dealership?: { mc_dealership_group_name?: string }
}
export type Verdict = 'consistent' | 'price_spread' | 'no_price' | 'fees_outside_price' | 'not_listed'
export class QuotaSpent extends Error {}
export const pause = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
async function search(params: Record<string, string>) {
const url = `${SEARCH}?${new URLSearchParams({ api_key: process.env.MARKETCHECK_API_KEY ?? '', ...params })}`
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(url)
if (res.ok) return await res.json() as { num_found: number, listings: Copy[] }
if (res.status !== 429) throw new Error(`Inventory Search returned ${res.status}`)
// Waiting clears the per-second limit, never a spent monthly quota
if (res.headers.get('Quota-Remaining') === '0') throw new QuotaSpent('monthly quota exhausted')
await pause(1000 * (Number(res.headers.get('Retry-After')) || 2 ** attempt))
}
throw new Error('still rate limited')
}
// One VIN in: a row per active copy, and the verdicts a person should check
export async function checkVin(vin: string, maxSpread = 0) {
const { num_found, listings } = await search({ vin, nodedup: 'true', rows: '50', sort_by: 'price', sort_order: 'desc' })
const rows = listings.map(copy => ({
source: copy.source,
dealer: copy.dealer?.name,
group: copy.mc_dealership?.mc_dealership_group_name,
price: copy.price,
msrp: copy.msrp,
fees: copy.fees,
includesFees: copy.price_includes_fees,
status: copy.vehicle_status,
firstSeen: copy.first_seen_at_date,
lastSeen: copy.last_seen_at_date,
link: copy.vdp_url
}))
const prices = rows.flatMap(row => (row.price === undefined ? [] : [row.price]))
const verdicts: Verdict[] = []
if (!rows.length) verdicts.push('not_listed')
if (prices.length && Math.max(...prices) - Math.min(...prices) > maxSpread) verdicts.push('price_spread')
if (rows.some(row => row.price === undefined)) verdicts.push('no_price')
if (rows.some(row => row.includesFees === false)) verdicts.push('fees_outside_price')
if (!verdicts.length) verdicts.push('consistent')
return { vin, found: num_found, rows, verdicts }
}| Verdict | Raised when |
|---|---|
consistent |
Every copy has a price within maxSpread of the others, and none leaves its fees out |
price_spread |
The highest and lowest prices differ by more than maxSpread. The default of 0 flags any difference, so agree any tolerance with your compliance team |
no_price |
A copy has no price key. The docs give Coming Soon units as one reason, which the status column shows |
fees_outside_price |
A copy's price_includes_fees is false. The field does not say which fees, and government-required charges may stay out, so a person reads the page |
not_listed |
No active copy on any site MarketCheck crawls, or none captured yet |
A copy under another dealer's name can be inventory sharing; the dealer column shows whose page it is.
What the documented sample shows
The docs' response for this VIN with nodedup=true holds two copies of a used 2023 Ford F-150 XLT, trimmed here with source, vdp_url and the dealer blocks removed:
{
"num_found": 2,
"listings": [
{ "id": "1FTFW1E81PKE39204-d5758666-5286", "heading": "Used 2023 Ford F-150 XLT",
"price": 39599, "msrp": 39599, "ref_price": 39994, "price_change_percent": -0.99,
"stock_no": "STP1373", "seller_type": "dealer",
"first_seen_at_date": "2025-07-16T02:18:53.000Z", "last_seen_at_date": "2025-07-18T02:15:27.000Z" },
{ "id": "1FTFW1E81PKE39204-8f4889a3-7d9c", "heading": "Used 2023 Ford F-150 XLT",
"price": 39599, "msrp": 39599, "ref_price": 39994, "price_change_percent": -0.99,
"stock_no": "STP1373", "seller_type": "dealer",
"first_seen_at_date": "2025-07-16T17:03:47.000Z", "last_seen_at_date": "2025-07-17T16:51:01.000Z" }
]
}Both copies ask 39,599 after the same cut from a ref_price of 39,994, and they share a stock number. The dealer blocks put one on the group's website and the other on one of its stores' websites. Without nodedup, the documented search returns only the first, with a num_found of 1.
checkVin calls this car consistent, but look at the capture times: MarketCheck first saw the new price on the group's site at 02:18 UTC on Jul 16, 2025, and on the store's site at 17:03 UTC the same day. A check between those captures could have seen the two a price cut apart, so a spread goes to a person with both links before anyone changes a price. History by VIN shows the price path behind one.
The nightly batch for a group
- Nightly, after the 11:00 UTC publish
- VINs from your DMS
- Every copy of each VIN
- Review queue
- Pricing manager approves fixes
import { checkVin, pause, QuotaSpent } from './vin-price-check'
// A group's VINs from the DMS, one call each. The result is a review queue: nothing here changes a price.
export async function nightly(vins: string[], maxSpread = 0) {
const review: object[] = []
for (const [i, vin] of vins.entries()) {
try {
const result = await checkVin(vin, maxSpread)
if (!result.verdicts.includes('consistent')) review.push(result)
} catch (err) {
// A spent quota stops the run and hands back the VINs it did not reach
if (err instanceof QuotaSpent) return { review, unchecked: vins.slice(i) }
review.push({ vin, error: String(err) })
}
await pause(250) // at most 4 calls a second, under the Free and Basic limit of 5
}
return { review, unchecked: [] }
}The data is published once a day, on or before 11:00 UTC, so one run a day is enough; an overnight run in the US leaves the queue for the morning. Each VIN costs one call, so the Free tier's 500 calls a month cover a nightly pilot of 16 VINs (16 calls a night for 31 nights is 496). The loop stays under the rate limit and retries a per-second 429 after Retry-After; a spent monthly quota, which Quota-Remaining identifies, stops it.
The pricing manager decides what the actual price is and approves each change, which the website provider or feed vendor makes. The next run confirms the fix or raises the VIN again. Run yesterday's sold VINs through checkVin too: any row that comes back points to a page that may still advertise a car you no longer have, a case FAQ 10 covers.
What the check cannot see
- Fees that never reach the listing.
feescovers only what the page shows. A fee the dealer requires belongs in the advertised price (FAQs 2 and 6), and one added only at the desk is invisible here. - Whether the shared price is right.
consistentmeans the copies agree with each other. Checking it against your own all-in number, fees included, takes your DMS and your compliance team. - Add-ons. FAQ 9 says dealers cannot present an optional add-on as required or charge for options the buyer did not agree to. Add-ons sold at the desk appear in no listing.
- Lag. Each site is crawled once a day, so a row shows the page as it stood at the last crawl.
- Prominence. The actual price must be the most prominent amount on the page (FAQs 4 and 5). The API returns the amounts without the layout, so a person at
vdp_urljudges which one stands out. - Other channels. The FAQs also cover social media, print, roadside signs, calls and texts, none of which a listing search reaches.
Gotchas
- Missing values are left out. No
pricekey means no price, and no fee fields means none were identified. Skiphas_price=true, which hides the copies with no price. rowstops out at 50. Ask for more and you get 10 with no error. Iffoundexceeds the rows returned, page withstart.
Start here
- Create a free account and copy your API key. Start on the Free tier for development: 500 calls a month at 5 calls per second. Plans are on the pricing page.
- Run the documented request for VIN 1FTFW1E81PKE39204 with and without
nodedup=true, then try one of your own VINs. - Point
nightlyat a VIN export from your DMS, and give the review queue an owner. - Keep the docs' consistency check and fee fields open, and read the FAQs with your counsel.