MarketMatchWholesaleRemarketingREST API

Put every wholesale car in front of the dealer most likely to retail it

MarketMatch ranks how well a car fits a dealer's inventory in both directions, and the same two calls can route off-lease units, wholesale trades, repossessions and auction lanes.

MarketCheck6 min read

For developers at remarketing companies, lenders, auctions and dealer groups

The MarketMatch docs make the case that a car placed with a dealer that is not a natural fit for it takes longer to sell and costs more to hold. Fit, as the API scores it, covers the dealer's history with that year, make and model and what the dealer has in stock today.

MarketMatch scores that fit from dealer inventory patterns, sales velocity and year, make and model affinity, and the docs say rankings are updated daily. It works in both directions: which dealer should get this car, and which cars should this dealer buy.

In short. You bring the candidates and MarketMatch ranks them. POST /v2/marketmatch/dealers/rank takes one VIN, with its price and miles, plus up to 100 candidate dealers. POST /v2/marketmatch/vins/rank takes one dealer plus up to 20 VINs. Files of car and dealer pairs go to the batch job under /v2/batch/marketmatch/rank/jobs. A person still makes every offer and approves every transfer.

What a ranking call returns

Both endpoints take a JSON body, with your key as the api_key query parameter. A dealer is its website domain in source, plus zip or mc_location_id when the dealer has more than one location. A vehicle is a 17-character vin with price in USD and miles, both whole numbers.

Every response carries two arrays. ranked_matches is sorted by vin_rank, best match first, and each row carries three numbers:

  • vin_score, on a documented 400 to 3000 scale, is the car's market position from its price, mileage and market conditions.
  • dealer_ymm_score, on the same scale, is the dealer's historical affinity for that year, make and model.
  • vin_rank is the prescriptive ranking. Lower is better, and 1 is the strongest match.

failed_items holds the rows the API could not score, with null scores. The docs name the usual causes, a dealer it could not match or a VIN it could not decode, and say the rest of the request is still ranked.

Here is the documented sample for one VIN against three candidate dealers, trimmed to the ranking fields:

Response (trimmed)json
{
  "ranked_matches": [
    { "source": "mbofaugusta.com", "zip": "30907", "mc_location_id": "1446052",
      "vin_score": 1680, "dealer_ymm_score": 0, "vin_rank": 1731 },
    { "source": "acceleride.mbofaugusta.com", "zip": "30907", "mc_location_id": "1446052",
      "vin_score": 1680, "dealer_ymm_score": 0, "vin_rank": 1731 },
    { "source": "acceleride.esterobaychevrolet.com", "zip": "33928", "mc_location_id": "1396240",
      "vin_score": 1680, "dealer_ymm_score": 0, "vin_rank": 1731 }
  ],
  "failed_items": []
}

Two details in this sample shape the code. The first two rows are one store's main domain and a subdomain of it, and both resolve to mc_location_id 1446052, so deduplicate by location before anyone sees the list. All three candidates also tie on vin_rank and score 0 on dealer_ymm_score, below the documented range: keep a tiebreaker of your own, such as transport cost, and send zero-affinity rows to a person.

One helper for both directions

marketmatch.tsts
const BASE = 'https://api.marketcheck.com/v2/marketmatch'

export interface Vehicle { vin: string, price: number, miles: number }
// source is the dealer's website domain; a multi-location dealer also needs zip or mc_location_id
export interface Dealer { source: string, zip?: string, mc_location_id?: string }
export interface Match extends Vehicle {
  source: string
  zip: string
  mc_location_id: string
  vin_score: number | null
  dealer_ymm_score: number | null
  vin_rank: number | null
}

async function rank(path: 'dealers/rank' | 'vins/rank', body: object) {
  const url = new URL(`${BASE}/${path}`)
  url.searchParams.set('api_key', process.env.MARKETCHECK_API_KEY ?? '')
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  })
  // A 422 body says what to fix: failed_vins, missing_dealers or multi_location_sources
  if (!res.ok) throw new Error(`MarketMatch ${path} returned ${res.status}: ${await res.text()}`)
  return await res.json() as { ranked_matches: Match[], failed_items: Match[] }
}

// Which dealer should get this car?
export async function rankDealers(car: Vehicle, dealers: Dealer[]) {
  if (dealers.length > 100) throw new Error('rank at most 100 dealers per request')
  const { ranked_matches, failed_items } = await rank('dealers/rank', { vin: car, dealers })
  // Rows arrive best first, and two domains can resolve to one location
  const byLocation = new Map<string, Match>()
  for (const match of ranked_matches) {
    if (!byLocation.has(match.mc_location_id)) byLocation.set(match.mc_location_id, match)
  }
  return { shortlist: [...byLocation.values()], unscored: failed_items }
}

// Which cars should this dealer buy?
export async function rankVehicles(dealer: Dealer, cars: Vehicle[]) {
  if (cars.length > 20) throw new Error('rank at most 20 VINs per request, or use a batch job')
  const { ranked_matches, failed_items } = await rank('vins/rank', { dealer, vins: cars })
  return { buyList: ranked_matches, unscored: failed_items }
}

The docs cap each request at 100 dealers or 20 VINs. Rank candidates that must be compared with each other in the same request: the docs do not say whether vin_rank values from separate requests compare, and the samples do not settle it, since the US samples return values in the thousands while the Canadian sample returns 1 and 2.

Remarketing and off-lease routing

Goal. An off-lease or fleet return lands, and the remarketing desk wants the few dealers most likely to retail it before the car ships anywhere.

  1. Return lands (VIN, price, miles)
  2. Dealers near the car
  3. Rank dealers for the VIN
  4. Transport and offer rules
  5. Remarketing manager sends offers
  1. Build the candidate list. Your grounding or buyer network may already be one. If not, Dealers Search (GET /v2/dealers/car) with zip, radius and dealer_type returns dealers near the car, 50 to a page at most. Each record's inventory_url is the domain MarketMatch expects as source, and passing its zip along settles which location you mean.
  2. Call rankDealers with the car and up to 100 candidates. Price feeds vin_score, so send the price you intend to ask and keep it consistent across a run.
  3. Apply your own rules in code, starting with transport cost from where the car sits and dealer credit.
  4. Put the shortlist in front of the remarketing manager, who sends the offers.

Dealer-to-dealer wholesale

A dealer group meets this problem from both sides. An aged unit at one rooftop may be a fresh unit at another, and the docs describe that network use directly: rank the group's own stores for the car, then move it by transfer. From the other side, trade-ins another store does not want and dealer-to-dealer offers are candidate buys, and rankVehicles ranks up to 20 of them for one store.

  1. Aged units and wholesale offers
  2. Rank rooftops or rank VINs
  3. Recon and gross rules
  4. Used-car manager approves the trade

Your recon cost and gross targets decide whether a well-ranked car still works at its asking price. The used-car manager approves each transfer and each buy.

Unusual: a lender routing repossessions

The docs list finance companies among the intended users, with a floor plan risk use case. A lender's recovery team has a related question: once a repossessed car is cleared for sale, which dealer in its network should get the first call? The request is the same as for an off-lease car, with the lender's dealer network as the candidate list.

  1. Recovered unit cleared for sale
  2. Rank the lender's dealer network
  3. Recovery and notice rules
  4. Remarketing desk calls the top fits

Your recovery process and its notice rules decide when a car may be offered, and the ranking sets the order in which the desk calls dealers.

Unusual: an auction's pre-sale lane pick

An auction knows which dealers bid in which lane or sale. For each consignment it can rank those dealers, place the car in the lane whose regular buyers fit it best, and send the pre-sale notice to the best fits.

  1. Consignments and lane buyer lists
  2. Batch rank car and buyer pairs
  3. Score each lane by its best fits
  4. Sale manager sets the run order

A full run list is too many pairs for single calls, so this is a batch job: one CSV row per car and buyer, with vin, price, miles, source and zip, compressed with gzip. The docs describe the output as your input columns plus ranking columns without naming them, so read the header row before you write the lane scoring.

batch_rank.pypython
import gzip
import hashlib
import os
import time

import requests

JOBS = "https://api.marketcheck.com/v2/batch/marketmatch/rank/jobs"
KEY = {"api_key": os.environ["MARKETCHECK_API_KEY"]}


def submit(csv_path, job_key):
    # One row per car and dealer pair: vin,price,miles,source,zip. Plain CSV is rejected.
    with open(csv_path, "rb") as f:
        payload = gzip.compress(f.read())
    r = requests.post(
        JOBS,
        params=KEY,
        # One key per intended job: resubmitting with a used key returns the original job
        headers={"Idempotency-Key": job_key},
        files={"file": ("pairs.csv.gz", payload, "application/gzip")},
        data={"client_reference": job_key},
        timeout=120,
    )
    r.raise_for_status()  # 409 active_job_exists: wait for a running job to finish
    return r.json()["job_id"]


def wait(job_id):
    while True:
        r = requests.get(f"{JOBS}/{job_id}", params=KEY, timeout=60)
        r.raise_for_status()
        job = r.json()
        if job["status"] != "PROCESSING":
            return job  # COMPLETED, or FAILED with error_code and error_message
        time.sleep(60)  # the documented polling interval


def download(job_id, out_path):
    # Each call uses one of the job's three downloads, so keep the first file
    r = requests.post(f"{JOBS}/{job_id}/download", params=KEY, timeout=300)
    r.raise_for_status()
    if hashlib.sha256(r.content).hexdigest() != r.headers["X-File-Checksum"].lower():
        raise ValueError("checksum mismatch")
    with open(out_path, "wb") as f:
        f.write(r.content)

Batch ranking is an Enterprise offering, enabled on request. Instead of polling, you can pass webhook_url and webhook_secret when you submit, then verify each notification's X-Webhook-Signature, an HMAC-SHA256 over the timestamp and the raw body.

Limits to design around

  • You supply the candidates. MarketMatch ranks only the dealers or VINs you send.
  • Name the location. A multi-location dealer without zip or mc_location_id fails the request with a 422, and the body lists the problem in multi_location_sources, missing_dealers or failed_vins.
  • Read failed_items on every call. A 200 can still leave candidates unscored.
  • Rankings change daily. Cache a result for the day.
  • Whole numbers only. price must be a positive integer and miles a non-negative one.
  • Canada is one parameter away. Add country=ca, send Canadian postal codes as zip, and read prices in CAD.
  • Batch has its own rules. Gzip CSV only, up to 100 MB. The Batch API guide allows 10 active jobs per operation, and one more returns 409 active_job_exists. Poll every 60 seconds. Each job allows three downloads, and results expire 30 days after the job finishes, when downloads return a 410.
  • Duplicate VINs are expected. successful_count counts unique VINs, so it can land below item_count.
  • Check the ranking against your own history. Before it drives a routing rule, compare it with where your past units sold and how long each one took.

Who this is for

Remarketing and wholesale platforms can put the dealer ranking behind every return. Lender recovery teams and floor plan analysts can run the same calls against their dealer networks. Auction software teams will want the batch job, and dealer-group developers can use both directions for transfers and buying.

Start here

  1. Start on the Free tier for development, and check the pricing page for the plan that includes MarketMatch. Ask about batch ranking if you work in files.
  2. Send the documented three-dealer request with your key and compare the response with the sample above.
  3. Build a candidate list from Dealers Search and rank a handful of recent returns.
  4. Keep the MarketMatch reference and the Batch API guide 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.

More use cases

All use cases