DealersTerritory mappingREST APILenders

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/car by inventory_url (a bare domain) or by zip and radius, and keep mc_dealer_id and mc_location_id as your durable keys for inventory, past inventory and syndication calls. The older /v2/dealers/car still supplies the dealer_id many 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 by mc_website_id, mc_dealer_id, mc_location_id and mc_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. Its id is the value dealer_id filters expect, and its records carry listing_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:

Response (trimmed)json
{
  "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

  1. Dealer website from your CRM
  2. Reduce it to a bare domain
  3. Both directories by inventory_url
  4. 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.

dealers.tsts
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.

  1. Territory ZIPs or metro codes
  2. Dealerships in each area
  3. Dedupe by mc_location_id
  4. 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

  1. Your rooftops and their ZIPs
  2. Dealerships within the radius
  3. Drop your own group
  4. Dealers listing your makes nearby
  5. Competitor set with IDs

For each of its rooftops, a dealer group can build a competitor list once and refresh it monthly:

  1. Call dealershipsNear(zip, radius) for the rooftop.
  2. Drop rows whose mc_dealership_group_id matches your own group.
  3. 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 same zip and radius, facets=mc_dealer_id|0|200 and rows=0. It returns which dealers list that make nearby and how many units each has.
  4. Keep the intersection, with mc_dealer_id, mc_location_id and 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.

  1. Lender's dealer list
  2. Match by website first
  3. Then by ZIP and address
  4. Analyst reviews the leftovers
  5. 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:

match_dealers.pypython
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/inventory returns 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=true keeps only its own.
  • GET /v2/dealerships/inventory/marketplaces/{marketplace_name} returns the same inventory formatted for facebook or google-va-feed. The docs call the output a suggested field mapping to validate before any upload.

Limits to design around

  • Pages of 50. rows defaults to 10 and tops out at 50. Page with start, and expect a 422 past your plan's pagination depth, so split big territories by state, msa_code or dealer_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 status on 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, longitude and 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

  1. Create a free account and copy your API key. Start on the Free tier for development; current plans are on the pricing page.
  2. Run resolveDealer on your own store's website and check the rooftops it returns.
  3. Run dealershipsNear on your ZIP and compare the list with the competitors you already know.
  4. Read the Dealerships Search and Dealers Search docs.

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