CompliancePricingDealersREST API

The price online should be the price at the desk

A daily checker on MarketCheck APIs flags listings with no price, prices that leave out identified fees, prices below your own all-in number and one VIN advertised at different prices, and a person approves every fix.

MarketCheck6 min read

For developers and compliance leads at dealer groups and dealer-website vendors

The same car can be advertised on the store's own website and on every site its inventory is syndicated to, and each copy carries its own price. When the copies disagree, or a fee sits outside the number, a shopper can arrive expecting a price the store will not honor.

On Sep 15, 2026 the FTC published price-transparency FAQs for auto dealers, and California's CARS Act takes effect Oct 01, 2026. The checker below finds the listings a person should look at. Whether any of them breaks a rule stays a question for your compliance team and your counsel.

In short. List your own inventory with Dealership Inventory Syndication, then pull every active listing of each VIN with Inventory Search, using vin with nodedup=true. Flag listings with no price, listings whose price_includes_fees is false, prices below your own all-in number and VINs with more than one price. History by VIN adds a price timeline for leads. A person reviews every flag and approves every change.

What the sources say

  • The FTC. Its Sep 15, 2026 announcement says the advertised price of a vehicle must be "the actual price that any consumer can walk in and pay to purchase the vehicle, excluding only charges that the government requires the consumer to pay." The FAQs cover dealer document fees, which belong inside the advertised price. RV Business reported the FAQs on Sep 16, 2026.
  • California. SB 766, the Combating Auto Retail Scams (CARS) Act, was signed in Oct 2025 (CalMatters Digital Democracy) and takes effect Oct 01, 2026 (California Dealer Academy). A compliance summary from California's Credit Unions describes it as requiring dealers to advertise a total price that includes dealer fees and excludes certain taxes and government fees. CDK Global's Sep 22, 2026 brief covers the same change, along with a 3-day right to cancel on used vehicles that a listing check cannot see.
  • The MarketCheck docs. The Inventory Search reference describes a cross-channel price consistency check as a common compliance review for US dealers. It adds that the check flags price differences for review without determining compliance with FTC or state rules. For compliance questions, it points to your own counsel.

What the checker reads

Step Endpoint Fields it uses
Your inventory GET /v2/dealerships/inventory with source and owned=true vin, price, stock_no
Every copy of a VIN GET /v2/search/car/active with vin and nodedup=true source, price, fees, price_includes_fees, vdp_url
One listing in full GET /v2/listing/car/{listing_id} price, msrp, fees, price_includes_fees, ref_price
The price timeline GET /v2/history/car/{vin} price, source, first_seen_at_date, last_seen_at

It takes two steps because Inventory Search switches to analytics mode when you pass a dealer parameter such as source: it forces rows=0 and returns counts and stats. The docs send you to Dealership Inventory Syndication for the listings themselves, with owned=true for the ones the dealer owns. The per-VIN search then returns the store's own listing plus the copies on sites its inventory is syndicated to, including listings that are not normally searchable. On its own, that search is the one-VIN price consistency check from Sep 15, 2026, which this checker runs for every unit you own.

The fee fields arrived with the Sep 15, 2026 docs update. When fee information is identified on a listing page, the listing carries fees, the total of the identified fees, and price_includes_fees, whether the advertised price includes them.

The daily audit: four flags per VIN

  1. Daily, after 11:00 UTC
  2. Your listings from syndication
  3. Every copy of each VIN
  4. All-in prices from your DMS
  5. Pricing manager approves fixes

The syndication data refreshes on or before 11:00 AM UTC, so run after that. Your DMS supplies the number only you have: the all-in price for each unit, meaning the amount a buyer must pay before government charges, as your pricing and compliance team defines it. The audit raises four flags:

  • No price. A copy carries no price key.
  • Fees outside the price. price_includes_fees is false on a copy.
  • Below your all-in price. A copy advertises less than the DMS number.
  • More than one price. Copies of the same VIN advertise different prices at the same time.
price-audit.tsts
const BASE = 'https://api.marketcheck.com/v2'

async function get(path: string, params: Record<string, string | number | boolean>) {
  const url = new URL(BASE + path)
  url.searchParams.set('api_key', process.env.MARKETCHECK_API_KEY ?? '')
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value))
  const res = await fetch(url)
  if (!res.ok) throw new Error(`${path} returned ${res.status}`)
  return res.json()
}

interface Listing {
  id: string
  vin: string
  source: string
  vdp_url?: string
  price?: number // left out when the page shows no price
  fees?: number // present only when fees were identified on the page
  price_includes_fees?: boolean
}

export interface Flag { vin: string, source: string, link?: string, reason: string }

// Your VINs: listings your store owns on its own domain. Advance by what came back,
// because the API caps rows at your plan's limit.
export async function ownVins(domain: string) {
  const vins = new Set<string>()
  for (let start = 0; ;) {
    const page = await get('/dealerships/inventory', { source: domain, owned: true, start, rows: 1500 })
    const listings = page.listings as Listing[]
    for (const l of listings) vins.add(l.vin)
    start += listings.length
    if (listings.length === 0 || start >= page.num_found) return [...vins]
  }
}

// Every active copy of one VIN: your site plus the sites that syndicate it
export async function auditVin(vin: string, allInPrice?: number) {
  const { listings } = await get('/search/car/active', { vin, nodedup: true, rows: 50 }) as { listings: Listing[] }
  const flags: Flag[] = []
  const flag = (l: Listing, reason: string) => flags.push({ vin, source: l.source, link: l.vdp_url, reason })

  const prices = new Set(listings.flatMap(l => (l.price === undefined ? [] : [l.price])))
  for (const l of listings) {
    if (l.price === undefined) flag(l, 'no advertised price')
    else if (allInPrice !== undefined && l.price < allInPrice) flag(l, `advertised ${l.price}, below your all-in ${allInPrice}`)
    if (l.price_includes_fees === false) flag(l, `price leaves out ${l.fees ?? 'identified'} in fees`)
    if (l.price !== undefined && prices.size > 1) flag(l, `one of ${prices.size} different prices for this VIN`)
  }
  return flags
}

export async function dailyAudit(domain: string, allIn: Map<string, number>) {
  const flags: Flag[] = []
  for (const vin of await ownVins(domain)) flags.push(...await auditVin(vin, allIn.get(vin)))
  return flags // into a review queue: nothing here changes a price
}

In the documented Dealership Inventory Syndication sample, trimmed here to two of its five listings with the dealer fields and links removed, the first listing has no price key, which is how a missing value arrives:

Response (trimmed)json
{
  "num_found": 1510,
  "listings": [
    { "id": "JTDVPMAE2MJ159394-02020730-b5c6", "vin": "JTDVPMAE2MJ159394",
      "heading": "2021 Toyota Corolla LE", "miles": 49410, "inventory_type": "used", "stock_no": "X06158" },
    { "id": "1FBZX2ZM1FKB23540-00524929-62c9", "vin": "1FBZX2ZM1FKB23540",
      "heading": "2015 Ford Transit-350 XL w/ Privacy glass", "price": 32732, "msrp": 32732,
      "ref_price": 32888, "ref_price_dt": 1757650120, "price_change_percent": -0.47,
      "miles": 20998, "inventory_type": "used", "stock_no": "T66848" }
  ]
}

The second listing shows the fields that record a price move. ref_price is the price previously listed at the same source, with ref_price_dt giving the time in Unix seconds, and price_change_percent measures the move.

What a person approves. Every fix. The pricing manager confirms each flag at its vdp_url and approves the change, which your website provider or feed vendor then makes.

Before a marketplace feed goes out

If you publish to Facebook Automotive Ads or the Google Vehicle Ads feed through /v2/dealerships/inventory/marketplaces/{marketplace_name}, audit that response before the upload. The docs call its field mappings suggestions that need validation and review before they go to a marketplace.

  1. Feed build for a marketplace
  2. Marketplace-format inventory
  3. Price and price-type checks
  4. Marketing manager releases the upload

A price of -1 means no price, and the documented Facebook sample shows it as the string "-1 USD", so parse the number before you compare. The Google format also carries vehicle_price_type, with all_in_price as the documented example value, and that label should match how your team built the number.

Unusual: the price moved between the lead and the visit

A shopper submits a lead at one price and walks in days later. If the price moved in between, the desk should know before the shopper sits down.

  1. CRM lead (VIN, price seen, time)
  2. Every copy of the VIN now
  3. Listing history by VIN
  4. Compare with the price seen
  5. Desk manager decides before the visit
lead_check.pypython
import os

import requests

BASE = "https://api.marketcheck.com/v2"
KEY = {"api_key": os.environ["MARKETCHECK_API_KEY"]}


def get(path, **params):
    r = requests.get(BASE + path, params={**KEY, **params}, timeout=120)
    r.raise_for_status()
    return r.json()


def since_the_lead(lead):
    """lead holds vin, price_seen and seen_at (unix seconds) from your CRM."""
    copies = get("/search/car/active", vin=lead["vin"], nodedup="true", rows=50)["listings"]
    history = get(f"/history/car/{lead['vin']}")  # newest first, up to 50 records a page
    return {
        "vin": lead["vin"],
        "price_seen": lead["price_seen"],
        # ref_price is the earlier price at the same site, from ref_price_dt
        "higher_now": [
            (c["source"], c["price"], c.get("ref_price"), c.get("vdp_url"))
            for c in copies
            if c.get("price") and c["price"] > lead["price_seen"]
        ],
        # Listing records that were live at some point after the lead came in
        "timeline": [
            (h["first_seen_at_date"], h["source"], h.get("price"))
            for h in history
            if h["last_seen_at"] >= lead["seen_at"]
        ],
    }

Run it the evening before an appointment, and the desk manager sees every copy now priced above what the shopper saw, with its link and earlier price, in time to decide how to handle the visit.

Unusual: the weekly exception report

  1. Weekly, before the sales meeting
  2. Flags from the daily audits
  3. Group by reason and site
  4. Compliance manager signs off
  5. Website and feed fixes

Store every flag the daily audit raises, then roll the week into one report for the compliance manager:

Column Where it comes from
VIN and stock number The syndication listing
Site and link source and vdp_url on each copy
Advertised and all-in price The copy's price and your DMS
Reason The audit's flag
First flagged and days open Your flag store
Decision and who made it The compliance manager

Each row gets a decision and an owner. The site that carries the price fixes it, and the next daily audit confirms the fix or raises the flag again.

Limits to design around

  • Dealer parameters turn Search into analytics. Pass source or dealer_id to Inventory Search and it returns counts and stats with no listings, so list your inventory through syndication.
  • nodedup=true returns every copy. That includes listings that are not normally searchable, and your own listing will be among them.
  • Missing values are left out. No price key means no price. has_price=false finds these directly, and vehicle_status marks Coming Soon units, which the docs give as an example.
  • Fee fields need fee information on the page. As a filter, price_includes_fees=na finds listings where inclusion is undetermined, and the filter takes one value per request, so na,true returns a 400.
  • Check your spec copy. The fee fields reached the docs on Sep 15, 2026. A client or CI check generated from an older copy of the OpenAPI spec will not know them, so check the build date in its info.version.
  • One refresh a day. A price fixed this afternoon shows in tomorrow's data, so confirm fixes at vdp_url.
  • A mismatch can be timing. Copies are captured at different moments on different sites, so confirm at the source before anyone changes a price.
  • Free-plan pages are smaller. The syndication endpoint caps rows per request lower on the Free plan than on paid plans, which is why the code advances by what came back.
  • Size the pilot. A run costs the syndication pages plus one search per VIN. The Free tier's 500 calls a month at 5 calls per second covers a pilot on a handful of units.
  • A clean audit is not a compliance finding. The docs say so of the consistency check, and it holds for every flag here. Spot-check some unflagged VINs by hand each week, and tune the flags when people find problems the audit missed.

Who this is for

Developers at dealer groups and at the vendors who build dealer websites and inventory feeds, with the compliance manager as the reader of the weekly report. The lead check suits CRM and BDC tooling.

Start here

  1. Start on the Free tier for development: 500 calls a month at 5 calls per second. Plans are on the pricing page.
  2. Run the documented consistency request for one of your VINs: vin, nodedup=true, sort_by=price and sort_order=desc.
  3. Export all-in prices from your DMS and pass them to dailyAudit.
  4. If your coding assistant has the Docs MCP, ask it to read the Inventory Search page for the fee filters.
  5. Keep the consistency check, Dealership Inventory Syndication and History by VIN pages open while you build.

Start building on the Free tier.

The Free tier is for development and testing, on the same endpoints you will use in production. When you go live, pick a plan on the pricing page.

Building with a coding assistant? Give it the free Docs MCP, so it looks up documented parameters instead of guessing them.

More use cases

All use cases