Listing DetailsREST APIMarketplacesData Quality

Vehicle detail pages that notice when the listing behind them ends

Build vehicle detail pages on MarketCheck Car Listing Details, then use the same call to catch changes between pulls and to keep one record per car across sources.

MarketCheck5 min read

For developers building vehicle detail pages, listing monitors and aggregation pipelines

Car Listing Details, GET /v2/listing/car/{listing_id}, returns one dealer listing in full: price and mileage with their reference values, photos, options and features, the seller's comments, the dealer, and the decoded build. It is the follow-up call to a search, since Inventory Search and History by VIN both return listing IDs, and it answers for any listing ID, whether the listing is live or long gone.

That last property is useful, and it is also the trap. A listing ID names one version of a car's listing at one source. When the dealer changes the price or the mileage, MarketCheck expires that listing and starts a new one with a new ID, and the old ID keeps answering with the old price. A detail page that stores IDs will show a stale price unless it checks.

In short. Fetch details with the ID a search gave you, render from build, media, extra and dealer, and treat the ID as a snapshot. When its last_seen_at stops moving, the listing has ended, and a search on the VIN shows where the car went.

What one listing returns

This is the documented sample, trimmed to the fields discussed here. The dealer name, address, photo URLs from the dealer's site and seller comments are removed:

Response (trimmed)json
{
  "id": "5TFUW5F16KX782196-44367fcc-c2fb",
  "vin": "5TFUW5F16KX782196",
  "heading": "Used 2019 Toyota Tundra SR5",
  "price": 27995,
  "ref_price": 28889,
  "ref_price_dt": 1752653949,
  "price_change_percent": -3.09,
  "miles": 80203,
  "dom_active": 83,
  "last_seen_at_date": "2025-07-22T02:58:28.000Z",
  "first_seen_at_date": "2025-07-17T06:10:29.000Z",
  "first_seen_at_source_date": "2025-05-02T06:14:51.000Z",
  "first_seen_at_mc_date": "2018-09-08T10:46:10.000Z",
  "media": {
    "photo_links_cached": [
      "https://api.marketcheck.com/v2/image/cache/car/5TFUW5F16KX782196-44367fcc-c2fb/e93cff16913e9d2a75f3197d3ba15c7d?api_key=YOUR_API_KEY"
    ]
  },
  "extra": {
    "options": ["SR5 Package", "Rear Under-Seat Storage Compartment"],
    "features": ["Lane departure", "Wireless phone connectivity"],
    "high_value_features": [
      { "category": "Safety & Driver Assist", "description": "Anti Collision System", "type": "Standard" }
    ],
    "options_packages": ["RE", "SP"]
  },
  "mc_dealership": { "mc_location_id": 1391837, "mc_rooftop_id": 517078 },
  "build": { "year": 2019, "make": "Toyota", "model": "Tundra", "trim": "SR5", "version": "SR5 5.7L 4WD FFV Double Cab Std Bed" }
}

Read the dates together and they match the lifecycle rules. The truck has been on this dealer's site since May 02, 2025 (first_seen_at_source_date), and MarketCheck first saw the VIN on Sep 08, 2018 (first_seen_at_mc_date). The listing itself only began on Jul 17, 2025, a day after its reference price was recorded: the dealer cut the asking price from 28,889 to 27,995, and the cut started this listing.

Two blocks here never appear in an Inventory Search result, extra (options, features, seller comments, high-value features and option package codes) and car_location, so they are what the extra call buys you.

A vehicle detail page from one ID

  1. Shopper opens a car
  2. Your detail route
  3. Car Listing Details
  4. Photos, specs and dealer card

Fetch details when a shopper opens a car. A results page can run on search rows alone, which already carry price, mileage, photos and the dealer, while each details fetch is a call. Build the title and spec table from build, because heading is the dealer's own text. Plan for every block to be optional: in the documented feed-integration sample, the listing has no miles key and no car_location, and its extra holds only high-value features and option package codes.

Photos come in two lists. media.photo_links are the dealer's own URLs, and media.photo_links_cached are MarketCheck's copies, which the docs say exist only for used cars. The cached URLs carry your key by default and fall under a private caching policy that rules out public CDNs, so serve them through your own server.

Each high-value feature carries a category, a description and a type (Standard or Optional in the sample), which suits a short highlights strip. The full feature list is long (the untrimmed sample has 127 entries), so group or collapse it for display. For a "price reduced" badge, compare price with ref_price, the previous asking price at the same source, which ref_price_dt dates.

detail.tsts
const BASE = 'https://api.marketcheck.com/v2'
const ENDED_AFTER_DAYS = 3 // a car absent longer than this returns under a new ID

export async function vehicleDetail(listingId: string) {
  // listing_id is case-sensitive, so pass it exactly as the search returned it
  const res = await fetch(`${BASE}/listing/car/${listingId}?api_key=${process.env.MARKETCHECK_API_KEY}`)
  if (res.status === 422) return null // not a valid listing ID
  if (!res.ok) throw new Error(`Car Listing Details returned ${res.status}`)
  const l = await res.json()
  const b = l.build ?? {}
  const daysSinceSeen = (Date.now() / 1000 - (l.last_seen_at ?? 0)) / 86400

  return {
    title: [b.year, b.make, b.model, b.trim].filter(Boolean).join(' ') || l.heading,
    price: l.price, // may be absent
    miles: l.miles, // may be absent
    photos: l.media?.photo_links ?? [],
    cachedPhotos: l.media?.photo_links_cached?.length ?? 0, // proxy these, never send them to the browser
    highlights: (l.extra?.high_value_features ?? []).map((f: { description: string }) => f.description),
    features: l.extra?.features ?? [],
    comments: l.extra?.seller_comments ?? '',
    specs: { version: b.version, engine: b.engine, drivetrain: b.drivetrain, fuel: b.fuel_type },
    dealer: l.dealer && { name: l.dealer.name, phone: l.dealer.phone, city: l.dealer.city, state: l.dealer.state },
    // Silent this long, the ID cannot resume: look up the VIN for its successor
    ended: daysSinceSeen > ENDED_AFTER_DAYS
  }
}

The same function serves a CRM or lead-routing tool: when a lead arrives from a detail page, store the listing ID with the lead and attach the options and features for the salesperson.

Unusual use: change detection between two pulls

  1. Daily, after 11:00 UTC
  2. Details for each saved ID
  3. Compare with the last pull
  4. VIN search when an ID stalls
  5. Update or retire the page

Because price and mileage cannot change under one ID, a second pull of the same ID has only two interesting outcomes. Either last_seen_at moved forward, which means the listing is alive at the same price and mileage, and any other difference, such as a different photo count, is a content update to re-render. Or it did not move, which means the listing has ended and you need to find out why.

For that, search active listings on the VIN with nodedup=true, which returns every current listing of the car, duplicates included. A listing at the same source means the dealer changed the price or mileage (its ref_price shows the old price), and listings only at other sources mean the car is now advertised elsewhere. If nothing comes back, the car is off the market or briefly offline, and the lifecycle guide settles which after three days: a car that reappears after more than three days gets a new listing, so an ID silent that long is finished.

change_check.pypython
import hashlib
import os
import time

import requests

BASE = "https://api.marketcheck.com/v2"
KEY = os.environ["MARKETCHECK_API_KEY"]
THREE_DAYS = 3 * 86400


def get(path, **params):
    r = requests.get(BASE + path, params={"api_key": KEY, **params}, timeout=60)
    r.raise_for_status()  # retry 429s as your client does elsewhere
    return r.json()


def snapshot(listing):
    extra, media = listing.get("extra", {}), listing.get("media", {})
    return {
        "id": listing["id"],
        "source": listing.get("source"),
        "last_seen_at": listing.get("last_seen_at", 0),
        "price": listing.get("price"),
        "miles": listing.get("miles"),
        "photos": len(media.get("photo_links", [])),
        "comments": hashlib.sha1(extra.get("seller_comments", "").encode()).hexdigest(),
    }


def check(saved, vin):
    now = snapshot(get(f"/listing/car/{saved['id']}"))
    if now["last_seen_at"] > saved["last_seen_at"]:
        changed = [k for k in ("photos", "comments") if now[k] != saved[k]]
        return "live", now, changed
    # This ID has stopped updating, so look for the car's current listings
    found = get("/search/car/active", vin=vin, nodedup="true", rows=50).get("listings", [])
    same_source = [l for l in found if l.get("source") == saved["source"]]
    if same_source:
        return "replaced", snapshot(get(f"/listing/car/{same_source[0]['id']}")), []
    if found:
        return "elsewhere", {"sources": sorted({l.get("source") for l in found})}, []
    if time.time() - now["last_seen_at"] <= THREE_DAYS:
        return "missing", now, []
    return "gone", now, []

Key your own pages by VIN and source, with the current listing ID as an attribute, so a replacement updates the page in place and a stored ID never strands it. And compare a Listing Details pull only with an earlier Listing Details pull. The docs note that this endpoint reads MarketCheck's main database as the crawl progresses, so its last_seen_at and first-seen fields can differ from the same listing in a search response.

Unusual use: one record per car across sources

  1. Listing IDs from several feeds
  2. Details for each ID
  3. Group by VIN
  4. Deduplicated VIN search
  5. Keep the attributed copy

Aggregators end up holding several IDs for one car: the dealer's own site plus copies on group sites and other sources. The VIN is the join key, so group by vin first. To choose which copy to keep, ask MarketCheck: a VIN search with default deduplication returns only the listing its attribution rules assign to the dealer with the car on its lot. Keep that copy and mark the rest as syndicated duplicates. For the past, History by VIN returns is_searchable per listing when you name it in fields, which marks the attributed copies among a car's old listings too.

When the attributed listing is not among the IDs you hold, Listing Details still helps. Copies whose mc_dealership.mc_location_id matches the attributed listing's location point at the same place, and the rest may be stale copies worth a second look. Leave listings without a VIN out of this process. The docs describe them as lower-quality records that are hard to normalize, and there is no reliable key to group them by.

Gotchas

  • listing_id is case-sensitive, and an invalid one returns a 422.
  • An expired ID still returns its last state, price included. Check last_seen_at before showing a price from a stored ID.
  • Missing values are left out of the response, so treat every field and block as optional.
  • Cached photo URLs carry your key unless you strip it. Keep them on your server and out of public caches.
  • Details cost a call each. Fetch on demand for detail pages, and batch the daily change check after the 11:00 UTC data update.
  • A 429 means a per-second rate limit or a spent monthly quota. The RateLimit-* and Quota-* headers tell them apart, and Retry-After says how long to wait.

Who this is for

Marketplace and classifieds developers get the detail page. Teams that mirror listings into their own systems, from lead-routing tools to data warehouses, get the change check and the de-duplication rules, which keep one current record per car.

Start here

  1. Start on the Free tier for development. Plans are on the pricing page.
  2. Fetch details for an id from any Inventory Search response and compare them with the search row: extra and car_location are the new parts.
  3. Save that pull and diff it against tomorrow's pull of the same ID.
  4. Read the Car Listing Details reference and the listing lifecycle guide, which explains when IDs change.

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.

More use cases

All use cases