Private partyAcquisitionREST APIMarketplaces

Find the private sellers near your store before they trade in down the road

Build a for-sale-by-owner search page, a daily buy-from-the-public list within 50 miles, and a private versus dealer asking-price report, all from one endpoint.

MarketCheck5 min read

For developers at car marketplaces, used-car stores and research teams

People selling their own cars post them on classifieds sites and marketplaces, one listing at a time and in their own words. MarketCheck collects those listings into Private Party Search, GET /v2/search/car/fsbo/active, with the same filters, facets and stats as Inventory Search. Private Party Listing Details, GET /v2/listing/car/fsbo/{listing_id}, returns one listing in full.

Marketplaces build for-sale-by-owner search pages on it. For a used-car store, the same data is a daily list of cars to buy from the public, found before the owners trade them in somewhere else.

In short. Search private-seller listings by ZIP and radius with the Inventory Search filters, then fetch details for the ones you show or pursue. There is no seller contact in the data, only the marketplace and the listing URL. Price each car against nearby dealer listings of the same build, and remember that both sides are asking prices.

What a private listing carries

The docs list what sets Private Party Search apart:

  • Every result has seller_type set to fsbo.
  • Listings carry no seller details such as name, phone or email, only the source marketplace and the listing URL.
  • Data is published daily with Inventory Search, by 11:00 AM UTC, and covers the US by default, with Canada on request.

Here is one listing, trimmed from the documented Private Party Listing Details sample:

Response (trimmed)json
{
  "id": "5XYKUDA60DG350915-b6084fe1-5314",
  "vin": "5XYKUDA60DG350915",
  "heading": "2013 Kia Sorrento EX AWD loaded $6800 obo |",
  "price": 6800,
  "miles": 131500,
  "msrp": 6800,
  "dom": 51,
  "dos_active": 1,
  "seller_type": "fsbo",
  "first_seen_at_mc_date": "2015-09-10T02:05:52.000Z",
  "first_seen_at_source_date": "2025-07-20T21:30:46.000Z",
  "source": "craigslist.org",
  "car_location": { "city": "Lake Geneva", "zip": "53147", "state": "WI" },
  "dealer": { "name": "Private Seller", "website": "craigslist.org" },
  "build": { "year": 2013, "make": "Kia", "model": "Sorento", "trim": "EX", "version": "EX AT 4WD" }
}

How to read it:

  • dealer describes the marketplace. On a private listing it holds the source site and a placeholder name. Contact happens through the listing page at vdp_url.
  • heading is the seller's text. Here it misspells the model and repeats the price. build is decoded from the VIN, so filter and display from build.
  • msrp echoes the price. In this sample msrp equals price, so take MSRP from a VIN decode.
  • The VIN has a past. MarketCheck first saw it on Sep 10, 2015, long before this listing appeared on Jul 20, 2025. History by VIN shows where it was listed in between.

A for-sale-by-owner search page

  1. Shopper sets filters
  2. Your search page
  3. Private Party Search
  4. Listing details
  5. Shopper contacts the seller

The search call takes the filters a shopper expects: zip and radius, make, model, year_range, price_range and miles_range. With a location, results sort nearest first and each listing carries dist in miles. The docs' own examples add min_photo_links=5 to show only listings with several photos, and carfax_1_owner=true for single-owner cars. For a filter sidebar, request facets=make,model,body_type on the same call and show the values with their counts.

Page with start and rows. Pages hold up to 50 rows, and the docs warn that asking for more than 50 falls back to the default of 10.

For the detail page, pass the search result's id to Listing Details. The docs note that the ID is case-sensitive and that an invalid one returns a 422, so store it exactly as returned. Add a "similar private-party cars" strip to the same page by passing the listing's VIN with vins and match=year,make,model.

For saved searches, store each shopper's filters and run them once a day after the data update with dos_active_range=0-1, the parameter the docs use for newer entrants. Email only the listings that are new since the last run.

Photos need one decision. photo_links points at the marketplace's own images. photo_links_cached points at MarketCheck's copies, and by default those URLs include your api_key. For a public page, pass append_api_key=false and fetch cached images through your own server, or use photo_links.

Unusual use: a buy-from-the-public list within 50 miles

A used-car store that buys directly from owners wants a short list each morning of recent private listings near the store whose asking price is under what the store would pay. The maximum offer comes from what dealers nearby ask for the same build.

  1. Daily, after 11:00 UTC
  2. Private sellers within 50 miles
  3. Same build at dealers nearby
  4. Your maximum offer
  5. Buyer reaches out
buy_from_the_public.pypython
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 private_sellers(store):
    """Private listings within 50 miles, listed in the last few days, with a price and miles."""
    return get("/search/car/fsbo/active", zip=store["zip"], radius=50,
               year_range=store["years"], miles_range=store["miles"], has_price="true",
               dos_active_range="0-3", sort_by="dos_active", sort_order="asc", rows=50)["listings"]

def dealer_median(car, store):
    """Median asking price of dealer listings of the same build near the store."""
    res = get("/search/car/active", vins=car["vin"], match="year,make,model,trim", car_type="used",
              zip=store["zip"], radius=store["retail_radius"], stats="price", rows=0)
    price = res.get("stats", {}).get("price", {})
    return price.get("median"), price.get("count", 0)

def buy_list(store):
    leads = []
    for car in private_sellers(store):
        median, comps = dealer_median(car, store)
        if not median or comps < store["min_comps"]:
            continue
        max_offer = median - store["recon"] - store["target_margin"]
        if car["price"] <= max_offer:
            leads.append({"listing_id": car["id"], "vin": car["vin"], "asking": car["price"],
                          "max_offer": round(max_offer), "dealer_median": median, "comps": comps,
                          "miles_away": car.get("dist"), "source": car["source"]})
    return sorted(leads, key=lambda lead: lead["max_offer"] - lead["asking"], reverse=True)

store holds your numbers: the ZIP, the years and mileage band you buy, your retail radius, a minimum number of comparables, and your recon cost and target margin. Fetch Listing Details for each lead to get the car's city and ZIP from car_location.

The data has no seller contact, so your buyer reaches out through the listing like any other shopper, within that marketplace's rules. Before the call, run History by VIN. Each earlier listing comes back with its seller_type, source, asking price and dates, so the buyer can see whether a dealer or an auction listed the same VIN recently. A car a dealer was asking for last month deserves a question about how the seller came to own it.

Unusual use: private versus dealer asking prices for a research desk

A research desk tracking the used market wants to know how far private sellers' asking prices sit from dealers' for the same cars. Because both endpoints share filters and stats, the comparison is two stats calls with identical filters.

  1. Models and markets to study
  2. Private-party price stats
  3. Dealer price stats
  4. Gap table with counts
price_gap.pypython
from buy_from_the_public import get

def median_and_count(path, **filters):
    price = get(path, stats="price", rows=0, **filters).get("stats", {}).get("price", {})
    return price.get("median"), price.get("count", 0)

def gap_row(ymm, state, miles_range):
    """ymm is a year|make|model string, such as the docs' 2019|Toyota|Camry."""
    filters = {"ymm": ymm, "state": state, "miles_range": miles_range}
    private, private_n = median_and_count("/search/car/fsbo/active", **filters)
    dealer, dealer_n = median_and_count("/search/car/active", car_type="used", **filters)
    return {"ymm": ymm, "state": state, "private_median": private, "private_n": private_n,
            "dealer_median": dealer, "dealer_n": dealer_n,
            "gap": dealer - private if private and dealer else None}

Publish the counts with every gap. Private-party results for a single model in a single state can be thin, and a median built on a handful of listings moves with every new post. Hold mileage in a narrow band so the two medians compare like with like.

The private-party endpoint removes duplicates by default, returning one listing per VIN, so a car posted on two marketplaces counts once in the stats. Pass nodedup=true when you want every copy, for example to count how many sites carry each car. Both medians are asking prices, so the gap measures how sellers price, and sale prices need another source.

Gotchas

  • Owners type the headings. Headings and comments come from the owner, so use build for anything you filter or join on.
  • Keys in photo URLs. Cached photo URLs include your api_key unless you pass append_api_key=false.
  • Flags depend on what the listing says. The docs describe carfax_1_owner and carfax_clean_title as set from what the listing page mentions, so false can mean the seller never said.
  • Canada is opt-in. The default is us. Pass country=ca on each call for Canadian listings, or country=all for both.
  • Premium rate, daily data. The docs note that Private Party Search is billed at a premium rate and updates once a day, so run it once after 11:00 AM UTC and cache it.
  • Paging and limits. Going past your plan's pagination depth returns a 422, and a 429 carries a Retry-After header to honor.

Who this is for

  • Marketplaces and classifieds sites adding a for-sale-by-owner section or a price guide for private sellers.
  • Used-car stores and buying centers that buy directly from the public.
  • Research and analytics teams comparing private and dealer asking prices.
  • Developers building acquisition tools for dealers.

Start here

  1. Create a free account and copy your API key. Start on the Free tier for development; plans are on the pricing page.
  2. Run one Private Party Search around your ZIP with has_price=true, and open a listing in Listing Details.
  3. Price a few private listings against Inventory Search comparables by hand before you set a margin.
  4. Schedule the daily run after 11:00 AM UTC.

The references are Private Party Search, Private Party Listing Details, Inventory Search and History by VIN.

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