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.amountis a number andprice.localizedis the display string. Use the number for analysis.mileage.valuepairs withmileage.unit, which wasMILEShere.vinidentifies the vehicle, andlisting_ididentifies 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_colorand an imagecount, while the first doesn't. Treat optional fields as optional in your code. pagination.has_moretells you whether to request the nextpage.
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.