Blog / API guides

CarGurus API: Search Used Car Listings and Dealers

The CarGurus API on Perceptlayer has four endpoints: makes, models, search and dealer. Each is a GET request with an X-API-Key header, each costs 1 credit on success, and each returns JSON. You look up a make ID, then a model ID, pass them to search to get listings, and use the service_provider_id from any listing to fetch its dealer.

What does the CarGurus API return?

The CarGurus data on Perceptlayer is structured used and new car inventory: one listing per vehicle, with the year, make, model, trim, price, mileage, VIN, seller and a link back to the listing page. The CarGurus API page lists all four endpoints, and the playground there lets you run a real call with your key before you write any code.

This guide follows the order you'd use in practice. Makes and models give you valid inputs, search gives you listings, and dealer gives you the seller behind a listing. The recorded response below comes from a real search call, so every field name you see is one the API returned.

The endpoints sit in the Cars area of the catalogue. If you want to know which listing fields suit price analysis, used car listing data: fields for price analysis covers that question separately.

Which endpoints does the CarGurus API have?

The CarGurus API has four endpoints: makes, models, search and dealer. All are GET requests and each costs 1 credit on success. Only makes and search take no required parameter; models needs a make_id and dealer needs a service_provider_id.

Endpoint Returns Credits Required Optional
/v1/cargurus/makes Makes 1 none locale, country_code, origin, device_platform
/v1/cargurus/models Models 1 make_id locale, country_code, origin, device_platform
/v1/cargurus/search Search 1 none make_model_trim_paths, price_min, price_max, distance, sort, sort_direction, page
/v1/cargurus/dealer Dealer 1 service_provider_id none

Authentication is the X-API-Key header with a key that starts with sk_live_. Where a header isn't possible, the api_key query parameter works instead. The authentication docs cover both.

How do you get valid make and model IDs?

Call makes first, then models with a make_id taken from the makes response. Search filters on IDs, not names, so this lookup is how you avoid guessing. In the recorded search response, the GMC Suburban appears as make_id m26 and model_id d126.

curl "https://api.perceptlayer.com/v1/cargurus/makes" \
  -H "X-API-Key: $API_KEY"

Then pass one of the returned make IDs to models:

curl "https://api.perceptlayer.com/v1/cargurus/models?make_id=m26" \
  -H "X-API-Key: $API_KEY"

We haven't recorded a makes or models response for this guide, so we won't show one. Run both calls in the playground on the Makes and Models pages to see the exact fields your key returns.

Both lookups also accept locale, country_code, origin and device_platform. They're optional strings, up to 20 characters for locale and 40 for the others. Nothing more is documented about them, so leave them out unless you've tested a value in the playground.

The IDs are inputs you reuse on every search, so cache them instead of repeating the lookups. Cache API responses in Python shows a TTL approach that fits.

How do you search used car listings?

To search used car listings, call /v1/cargurus/search with make_model_trim_paths set to the make ID and model ID joined by a slash, URL-encoded as %2F. Add distance, price bounds and a sort order to narrow the results. Each successful call costs 1 credit and returns one page of listings. This is the request we recorded:

curl "https://api.perceptlayer.com/v1/cargurus/search?make_model_trim_paths=m26%2Fd126&distance=50000&sort=best_match" \
  -H "X-API-Key: $API_KEY"

The same request in Python:

import os
import requests

resp = requests.get(
    "https://api.perceptlayer.com/v1/cargurus/search",
    headers={"X-API-Key": os.environ["API_KEY"]},
    params={
        "make_model_trim_paths": "m26/d126",
        "distance": 50000,
        "sort": "best_match",
    },
    timeout=30,
)
resp.raise_for_status()
data = resp.json()["data"]
for listing in data["listings"]:
    print(listing["listing_title"], listing["price"]["amount"], listing["mileage"]["value"])

requests encodes the slash for you. The response has data.listings, data.pagination and a meta object.

Which parameters does search take?

All seven are optional. Nothing about defaults or page sizes is documented, so the table lists only types and allowed values.

Parameter Type Notes
make_model_trim_paths string Up to 200 characters. Example: m26/d126
price_min integer Lower price bound
price_max integer Upper price bound
distance integer Search radius
sort string One of best_match, deal_score, price, mileage, distance, date
sort_direction string One of asc, desc
page integer Page number

What does a search response look like?

A search response has data.listings, data.pagination and a meta object, and each entry in data.listings is one vehicle with its price, mileage, VIN and seller. The excerpt below is trimmed from the recorded response for the request above, with … marking the cut parts.

{
  "success": true,
  "data": {
    "listings": [
      {
        "listing_id": 461194930,
        "listing_title": "1996 GMC Suburban",
        "listing_url": "https://www.cargurus.com/details/461194930",
        "vin": "3GKFK16R5TG503480",
        "year": 1996,
        "make": "GMC",
        "make_id": "m26",
        "model": "Suburban",
        "model_id": "d126",
        "trim": "K1500 4WD",
        "price": {
          "amount": 7995,
          "currency": "USD",
          "localized": "$7,995",
          "rating": "NA",
          "fees_compliant": true
        },
        "mileage": {
          "value": 175456,
          "unit": "MILES",
          "localized": "175,456"
        },
        "drivetrain": "Four-Wheel Drive",
        "fuel_type": "Gasoline",
        "days_on_market": 14,
        "distance": 537.75244,
        "seller": {
          "service_provider_id": "349337",
          "name": "Little Rock Auto Sales",
          "city": "Charlotte",
          "region": "NC",
          "location": "Charlotte, NC",
          …
        },
        "is_new": false,
        "is_cpo": false,
        "seller_type": "DEALER"
      },
      …
    ],
    "pagination": {
      "page": 1,
      "page_size": 21,
      "page_count": 1,
      "total": 21,
      "has_more": false
    }
  },
  "meta": {
    "duration_ms": 1556,
    "endpoint_family": "search",
    "market": "US"
  }
}

A few fields worth knowing, all from the recording:

  • price.amount is a number and price.localized is the display string. Use the number for analysis.
  • mileage.value pairs with mileage.unit, which was MILES here.
  • vin identifies the vehicle, and listing_id identifies the listing. Pick the key that matches your question: vehicle or advert.
  • Some fields appear on one listing and not another. The second listing in the recording has exterior_color, interior_color and an image count, while the first doesn't. Treat optional fields as optional in your code.
  • pagination.has_more tells you whether to request the next page.

How do you look up a dealer?

To look up a dealer, take seller.service_provider_id from a listing and pass it to /v1/cargurus/dealer, which costs 1 credit on success. The dealer ID in the recorded response is 349337, for the Charlotte, NC seller above. The parameter is a string of up to 20 characters.

curl "https://api.perceptlayer.com/v1/cargurus/dealer?service_provider_id=349337" \
  -H "X-API-Key: $API_KEY"

We haven't recorded a dealer response, so the fields it returns aren't shown here. The Dealer endpoint page lists it and its playground runs the call.

The listing already includes the seller's name, city, region and postal_code, so you only need the dealer call when you want the dealer record itself. Collect the distinct service_provider_id values from your search results first and call dealer once per ID, not once per listing.

What does a regional inventory pull cost?

A regional inventory pull costs one credit per successful call, so the cost is the call count: the lookups, plus one search call per page per model, plus one dealer call per distinct dealer. Failed calls aren't billed. The numbers below are an illustration for planning, not a measurement.

Say you track 40 models. The lookups are one makes call, plus one models call for each make involved. Take 10 makes: 1 + 10 = 11 credits, and you cache them.

Then search each model. If each model's results fit on 3 pages, that's 40 models × 3 pages = 120 credits per full pass. Add 25 distinct dealers at 1 credit each: 25 credits. One refresh is 120 + 25 = 145 credits.

Run that daily for 30 days: 30 × 145 = 4,350 credits. The free tier is 1,000 credits a month, and credits are prepaid in packs starting at 10,000, which last 12 months. The pricing page has what credits cost, and the credits docs explain how a call is debited.

Your own page count will differ. In the recorded response, the whole result fit on one page, with page_count 1 and total 21. Run a model once and read pagination.page_count before you multiply.

Failed calls cost nothing and return X-Credits-Used: 0. Successful responses carry X-Credits-Used and X-Credits-Remaining, so you can log the balance as a job runs.

How do you page through results safely?

To page through results safely, request page 1, read pagination.has_more, and keep going until it's false. Back off when you get a 429, using the Retry-After header, and don't retry blindly after a client timeout. Cap the number of pages per run so a bug can't spend credits without limit.

import os
import time
import requests

URL = "https://api.perceptlayer.com/v1/cargurus/search"
HEADERS = {"X-API-Key": os.environ["API_KEY"]}


def fetch_all(path, max_pages=20):
    listings = []
    for page in range(1, max_pages + 1):
        while True:
            r = requests.get(
                URL,
                headers=HEADERS,
                params={"make_model_trim_paths": path, "page": page},
                timeout=30,
            )
            if r.status_code in (429, 503):
                time.sleep(int(r.headers.get("Retry-After", "5")))
                continue
            break
        r.raise_for_status()
        data = r.json()["data"]
        listings.extend(data["listings"])
        if not data["pagination"]["has_more"]:
            break
    return listings


cars = fetch_all("m26/d126")
print(len(cars))

max_pages caps the spend of one run. Requests have no idempotency key, so a call whose reply was lost may have succeeded and been billed. Retry web API calls without paying twice covers that, and paginate a web data API in Python goes deeper on loops.

Rate limits are per account and counted per minute. Free accounts get 1 request a second, and paid packs include faster rates. See the rate limits docs and error codes for the full list.

What should you know before you build on it?

Data is collected from publicly available sources and returned as found at the time of the call. A listing can be sold or repriced after you fetch it, so treat a response as a snapshot and store the time you fetched it.

CarGurus is a marketplace for dealer inventory, and this API reads its listings. If you need a different region, AutoScout24 covers European listings. For your own inventory, your dealer management system is the system of record, so use it there rather than reading your own cars back from public listings.

For agents, the same endpoints are available as MCP tools. search_endpoints and get_endpoint only look up the catalogue and cost nothing; call_endpoint runs the call and spends 1 credit on success. The AI agents docs show the setup.

Ready to run it? Get a free API key, send the makes call above, and follow the IDs through to search. You can browse the CarGurus endpoints or the other API guides from there. For background on the source, see CarGurus, and for the header behind the backoff above, see the Retry-After definition in RFC 9110.

APIs in this post

Frequently asked questions

How much does the CarGurus API cost per call?

Each successful call to makes, models, search or dealer costs 1 credit. Failed calls cost nothing and return X-Credits-Used: 0. Every account gets 1,000 free credits a month after verifying an email, with no credit card. The pricing page shows what credits cost.

Can I search by price range?

Yes. search takes price_min and price_max as integers, and you can combine them with distance, sort and sort_direction. The sort parameter accepts best_match, deal_score, price, mileage, distance or date, and sort_direction accepts asc or desc.

Where do I get the service_provider_id for the dealer endpoint?

Read it from seller.service_provider_id in any search listing. In the recorded response, one listing's seller has service_provider_id 349337. Pass that string, up to 20 characters, to /v1/cargurus/dealer. Deduplicate IDs first so you pay for each dealer once.

What happens if I hit the rate limit?

You get a 429 rate_limited response with a Retry-After header giving the seconds to wait, and the call isn't billed. Limits are per account, counted per minute. Free accounts get 1 request a second. Read RateLimit-Remaining to pace a loop before you hit the limit.

Do I need a CarGurus account or key?

No. You need a Perceptlayer API key, sent in the X-API-Key header. Sign up for one free, and your 1,000 monthly credits cover about 1,000 calls at 1 credit each. The quick start walks through the first request.

  1. AutoScout24 API: Search European Car Listings as JSON

    Search AutoScout24 listings as JSON: make_id and price filters, a recorded BMW response, and what a monthly price sweep costs in credits.

  2. Cars.com API: Used Car Listings and Price Evaluations

    Search Cars.com listings with its six required parameters (zip, radius_miles, stock_type, sort, page, page_size), then listing, dealer and price-eval calls.

  3. Redfin API: Search Homes, Then Property and Valuation

    Four Redfin endpoints at 1 credit each: find a region, search homes with a real JSON response, then call property and valuation by property_id.

Start with 1,000 free credits

No password to set. We email you a sign-in link, and your key works on every API.

No credit card. 1,000 free credits a month.