From a dealer's website to its MarketCheck ID, and every rooftop around it
Resolve dealer IDs from websites, map territories by ZIP and radius, build a dealer group's competitor set and match a lender's dealer list, using the directory endpoints and the IDs that tie them to inventory.
MarketCheck5 min read
For developers at dealer groups, lenders and dealer-facing vendors
Most MarketCheck workflows start from a dealer: its inventory and the competitors around it. Before any of that you need the dealer's MarketCheck ID, while your own records usually hold a name and an address, sometimes with a website. The dealer directory endpoints close that gap for the dealers whose inventory MarketCheck monitors across the US and Canada.
In short. Search
/v2/dealerships/carbyinventory_url(a bare domain) or byzipandradius, and keepmc_dealer_idandmc_location_idas your durable keys for inventory, past inventory and syndication calls. The older/v2/dealers/carstill supplies thedealer_idmany search filters take.
Two directories, and which ID to keep
MarketCheck runs two dealer directories side by side.
- Dealerships Search,
GET /v2/dealerships/car, is the newer system. It ties each dealership's websites, locations, rooftops and group memberships into one structure, identified bymc_website_id,mc_dealer_id,mc_location_idandmc_rooftop_id, plus group and sub-group IDs and names. - Dealers Search,
GET /v2/dealers/car, and Dealer Details,GET /v2/dealer/car/{dealer_id}, are the older system, which the docs say is being phased out in favor of Dealerships Search. Each dealer there is typically one website, so multi-location groups may be under-represented. Itsidis the valuedealer_idfilters expect, and its records carrylisting_count, the dealer's number of active listings.
Listings carry both schemes, in a legacy dealer object and an mc_dealership object. In the documented listing samples, the legacy dealer.id matches mc_dealership.mc_website_id, which fits the one-dealer-per-website model; check that on your own dealers before you rely on it.
Which ID to store follows from how MarketCheck handles change. A domain redirect keeps the dealer and location IDs and creates a new website ID, while a physical move keeps the dealer and website IDs and updates the location. Key your records on mc_dealer_id and mc_location_id, and treat the domain and mc_website_id as attributes that can change.
Groups follow the same logic. When mc_dealership_group_id and mc_sub_dealership_group_id are equal, the dealer belongs directly to the group. After an acquisition, the group ID names the acquirer and the sub-group ID keeps the original group, so you can still report on it.
Here is the documented Dealerships Search sample for a 50-mile radius around a point in Los Angeles, trimmed to one row with the seller name, website, street and phone removed:
{
"num_found": 2766,
"mc_dealerships": [
{
"mc_website_id": "11025903",
"mc_dealer_id": "1174168",
"mc_location_id": "1445189",
"mc_rooftop_id": "969160",
"mc_category": "Dealer",
"status": "active",
"dealer_type": "independent",
"city": "Los Angeles",
"state": "CA",
"zip": "90014",
"latitude": "34.044465",
"longitude": "-118.252544",
"distance": 0.74
}
]
}Two details to code for: coordinates arrive as strings, and distance shows up on each row of a radius search even though the schema table does not list it.
Finding a dealer's ID from its website
- Dealer website from your CRM
- Reduce it to a bare domain
- Both directories by inventory_url
- Store the IDs against your record
inventory_url takes a bare domain, the way the documented examples write it, so strip the scheme, www. and any path first. Query both directories: the newer one returns every rooftop under that website, and the older one returns the dealer_id that search filters take.
const BASE = 'https://api.marketcheck.com/v2'
async function get(path: string, params: Record<string, string | number>) {
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()
}
// "https://www.Example-Motors.com/used/" -> "example-motors.com"
export function bareDomain(website: string) {
const url = new URL(website.includes('://') ? website : `https://${website}`)
return url.hostname.toLowerCase().replace(/^www\./, '')
}
export async function resolveDealer(website: string) {
const inventory_url = bareDomain(website)
const [current, legacy] = await Promise.all([
get('/dealerships/car', { inventory_url, rows: 50 }),
get('/dealers/car', { inventory_url })
])
return {
inventory_url,
rooftops: current.mc_dealerships, // one row per location or rooftop behind the site
dealerIds: legacy.dealers.map((d: { id: string }) => d.id) // what dealer_id filters take
}
}
// Every dealership within `radius` miles of a ZIP, nearest first (the default with a location)
export async function dealershipsNear(zip: string, radius: number, filters: Record<string, string> = {}) {
const found = []
for (let start = 0; ; start += 50) {
const page = await get('/dealerships/car', { zip, radius, start, rows: 50, ...filters })
found.push(...page.mc_dealerships)
if (page.mc_dealerships.length === 0 || start + 50 >= page.num_found) break
}
return found
}One website can return several rooftops. A retailer that sells from many locations through one site is the usual case, so pick the rooftop by ZIP or street when you need a single location. An empty result from both directories means MarketCheck does not monitor that domain; if the dealer's inventory pages are served from a different domain than its home page, try that one.
Territory mapping
A vendor or OEM field team can map every dealership in a territory with dealershipsNear, or swap the radius for state or msa_code.
- Territory ZIPs or metro codes
- Dealerships in each area
- Dedupe by mc_location_id
- Sales ops assigns reps
Facets give area counts without paging: facets=dealer_type or facets=mc_dealership_group_name with rows=0 returns how many franchise and independent dealers, or which groups, sit in the area. To tier accounts by size, the older directory takes listing_count_range and sorts by listing_count. Overlapping radii return the same rooftop more than once, so dedupe on mc_location_id before you count.
Two more filters help a vendor's sales team. created_at_days (or created_at_range) filters by when a directory record was created, so a monthly query surfaces dealerships that are new to the directory in your territory. And each row can carry an msa_code, which groups rooftops by metro for reporting.
A dealer group's competitor set
- Your rooftops and their ZIPs
- Dealerships within the radius
- Drop your own group
- Dealers listing your makes nearby
- Competitor set with IDs
For each of its rooftops, a dealer group can build a competitor list once and refresh it monthly:
- Call
dealershipsNear(zip, radius)for the rooftop. - Drop rows whose
mc_dealership_group_idmatches your own group. - The directory does not say which brands a dealer sells. To keep same-brand competitors only, run one Inventory Search per rooftop with your
make, the samezipandradius,facets=mc_dealer_id|0|200androws=0. It returns which dealers list that make nearby and how many units each has. - Keep the intersection, with
mc_dealer_id,mc_location_idand the bare domain for each competitor.
The IDs then plug into other endpoints. The domain goes into Past Inventory Search as source to see what left a competitor's lot, and mc_dealer_id (comma-separated for several dealers) goes into /v2/dealerships/inventory for their current listings.
Mapping a lender's dealer network
An indirect lender or floor-plan provider knows its dealers by its own IDs, names and addresses. Matching them to MarketCheck lets the lender see what each dealer currently advertises.
- Lender's dealer list
- Match by website first
- Then by ZIP and address
- Analyst reviews the leftovers
- Store the MarketCheck IDs
The script tries the website first and falls back to street or name inside the dealer's ZIP; anything ambiguous goes to a person:
import csv
import os
import re
import requests
URL = "https://api.marketcheck.com/v2/dealerships/car"
KEY = os.environ["MARKETCHECK_API_KEY"]
def search(**params):
r = requests.get(URL, params={"api_key": KEY, "rows": 50, **params}, timeout=60)
r.raise_for_status()
return r.json()["mc_dealerships"]
def norm(text):
# letters and digits only, so "123 Main St." and "123 main st" compare equal
return re.sub(r"[^a-z0-9]", "", (text or "").lower())
def match(dealer):
if dealer.get("website"):
domain = re.sub(r"^(https?://)?(www\.)?", "", dealer["website"].lower()).split("/")[0]
hits = search(inventory_url=domain)
if len(hits) > 1: # one site, several rooftops: keep the one in this ZIP
hits = [d for d in hits if d.get("zip") == dealer["zip"]]
if len(hits) == 1:
return "website", hits
nearby = search(zip=dealer["zip"], radius=5)
hits = [d for d in nearby if norm(d.get("street")) == norm(dealer["street"])
or norm(d.get("seller_name")) == norm(dealer["name"])]
return ("address" if len(hits) == 1 else "review"), hits
with open("lender_dealers.csv") as src, open("matched.csv", "w", newline="") as dst:
out = csv.writer(dst)
out.writerow(["lender_dealer_id", "method", "mc_dealer_id", "mc_location_id", "mc_website_id"])
for dealer in csv.DictReader(src): # columns: lender_dealer_id, name, street, zip, website
method, hits = match(dealer)
for hit in hits or [{}]:
out.writerow([dealer["lender_dealer_id"], method, hit.get("mc_dealer_id"),
hit.get("mc_location_id"), hit.get("mc_website_id")])Expect the review bucket to hold cases like two rooftops at one address, or a trading name that differs from the name in the lender's file. Once matched, /v2/dealerships/inventory with the dealer's mc_website_id (or its domain as source) and owned=true returns the listings attributed to that dealer, which a portfolio or floor-plan team can set against its own records. Re-run the match monthly: MarketCheck reconciles groups, acquisitions and status changes monthly, and IDs move with domains and addresses as described above.
Two syndication endpoints that take the same IDs
GET /v2/dealerships/inventoryreturns a dealer's complete active inventory, refreshed daily by 11:00 AM UTC, with far larger pages than standard search. How many rows a request may return depends on your plan. By default it includes listings the dealer shows but that are attributed elsewhere;owned=truekeeps only its own.GET /v2/dealerships/inventory/marketplaces/{marketplace_name}returns the same inventory formatted forfacebookorgoogle-va-feed. The docs call the output a suggested field mapping to validate before any upload.
Limits to design around
- Pages of 50.
rowsdefaults to 10 and tops out at 50. Page withstart, and expect a 422 past your plan's pagination depth, so split big territories bystate,msa_codeordealer_type. - Default order. Results come back by ID unless you pass a location, which sorts them by distance.
- Active dealers only. Dealers Search lists active dealers; check
statuson the rows you keep. - Brands are not in the directory. Use Inventory Search facets to learn what a dealer lists.
- Strings for numbers. Parse
latitude,longitudeand IDs defensively. - 429s. Honor
Retry-After;Quota-*headers tell you when the monthly quota, rather than the per-second limit, is the cause.
Who this is for
Developers at dealer groups mapping their competition, lenders and floor-plan providers matching their dealer networks, vendors onboarding dealers, and OEM field teams planning territories.
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.
- Run
resolveDealeron your own store's website and check the rooftops it returns. - Run
dealershipsNearon your ZIP and compare the list with the competitors you already know. - Read the Dealerships Search and Dealers Search docs.