Used Car Listing Data: Fields for Price Analysis
A used-car price model needs a target and six candidate features from every listing. The target is the advertised price. The features are mileage, first-registration year, make with model and trim, power or engine, seller type (dealer or private) and how long the car has been listed. The AutoScout24, CarGurus and AutoTrader UK search endpoints return most of these as named JSON fields. None returns all of them in the same shape, so the real work is normalising them.
Which listing fields matter for a used-car price model?
The advertised price is the target. The candidate features are mileage, first-registration year, make-model-trim, power or engine, seller type and listing age. Treat listing age as a feature to test rather than assume; your own data will show whether it carries signal. Everything else (colour, photo count, dealer name) is secondary until those six are clean.
The table maps the target and those features to what each endpoint returned in the recorded responses.
| Feature | AutoScout24 search |
CarGurus search |
AutoTrader UK search |
|---|---|---|---|
| Price | price.amount, price.currency (EUR) |
price.amount, price.currency (USD) |
price.amount, price.currency (GBP) |
| Mileage | mileage.value_km |
mileage.value, mileage.unit (MILES) |
not in the recorded excerpt |
| Year | first_registration.year, .month |
year |
tracking_context.year |
| Make, model | make, model, model_group |
make, model, make_id, model_id |
title, tracking_context.make, .model |
| Trim | motor_type (engine designation); variant is the body variant |
trim |
sub_title (free text) |
| Power | power.kw, power.ps |
power (hp), engine |
no power field in the recorded search record |
| Seller type | seller.type (Dealer, PrivateSeller) |
seller_type (DEALER) |
seller_type (PRIVATE) |
| Listing age | not in the recorded excerpt | days_on_market |
not in the recorded excerpt |
| Identifier | listing_id |
listing_id, vin |
advert_id |
Blank cells in this table mean the field wasn't in the recorded search response. Each marketplace also has a single-listing endpoint (listing with listing_id on AutoScout24, advert_id on AutoTrader UK). Run one in the playground on the API page to see what the detail view adds before you build around a gap.
How do you pull comparable listings from AutoScout24?
Call GET /v1/autoscout24/search with a make, a model and a price band, and page through the results. The endpoint filters on make_id, model_id, registration_year_min, registration_year_max, mileage_min, mileage_max and seller_type, so you can cut the sample to the cars you want to compare before any code runs. Each call costs 1 credit.
curl "https://api.perceptlayer.com/v1/autoscout24/search?market=de&make_id=13&model_id=1639&price_min=5000&price_max=20000&sort=price&page=2&limit=20" \
-H "X-API-Key: $API_KEY"
The make_id and model_id values come from the taxonomy endpoint (include[]=makes, include[]=models). Here is an excerpt of the recorded response, trimmed:
{
"price": { "amount": 5800, "currency": "EUR", "evaluation": 1, "is_evaluation_enabled": true },
"mileage": { "value_km": 177000, "unit": "km" },
"first_registration": { "raw": "01/2012", "year": 2012, "month": 1 },
"power": { "kw": 85, "ps": 116 },
"make": "BMW",
"model_group": "3er",
"variant": "Limousine",
"motor_type": "316d",
"seller": { "id": "…", "type": "PrivateSeller" },
…
}
That one record already carries price, mileage, year and month, power, body variant, engine designation and seller type. The pagination block in the same response reports total: 193 across total_pages: 10 at limit: 20, so the whole sample is 10 calls: 10 × 1 credit = 10 credits. The pagination guide covers safe page loops.
What do CarGurus and AutoTrader UK return instead?
CarGurus returns the richest record for pricing work in the recorded sample: a VIN, a trim, a drivetrain, days_on_market and a seller_type. AutoTrader UK returns a clean price and year but keeps trim inside a text string. Both are single calls per page at 1 credit, so the difference is in the fields, not the cost.
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"
From the recorded CarGurus response:
{
"listing_title": "1996 GMC Suburban",
"vin": "…",
"year": 1996,
"trim": "K1500 4WD",
"price": { "amount": 7995, "currency": "USD", "rating": "NA" },
"mileage": { "value": 175456, "unit": "MILES" },
"days_on_market": 14,
"seller_type": "DEALER",
…
}
The CarGurus search endpoint takes make_model_trim_paths (m26/d126 above is a make and model path), price_min, price_max, distance, sort and page. You can sort by date or deal_score as well as price and mileage. The makes and models endpoints list the IDs.
The AutoTrader UK search endpoint requires a postcode and filters on min_mileage, max_mileage, min_year_manufactured, max_year_manufactured and seller_type (private or trade). The recorded excerpt looks like this:
{
"title": "BMW 5 Series",
"sub_title": "2.0 518d SE Euro 6 (s/s) 4dr",
"price": { "amount": 2200, "currency": "GBP" },
"seller_type": "PRIVATE",
"tracking_context": { "make": "BMW", "model": "5 Series", "year": 2014, "price_indicator": "NOANALYSIS" },
…
}
Mileage isn't in that excerpt. You can still get at it: filter with min_mileage and max_mileage to build mileage bands, or sort by mileage. Set price_search_type=total so you collect full prices rather than monthly finance figures.
Where does cross-market comparison break down?
It breaks in five places: currency, mileage units, trim vocabulary, seller-type labels and the marketplace's own deal rating. Each is a mechanical fix, but only if you handle it before pooling the data. Skip any one and a "UK cars are cheaper" result may just be pounds against euros.
- Currency and units. Prices arrive as
EUR,USDandGBP. CarGurus mileage is in miles, AutoScout24 in kilometres. Convert mileage to one unit, and model each market's price in its own currency until you've chosen an exchange-rate source and date. - Trim. CarGurus gives
trimas a field. AutoScout24 has no trim field in the recorded record:variantis a body variant (Touring,Limousine) andmotor_typethe engine designation. AutoTrader UK embeds trim wording insub_title. Matching "the same car" across markets means parsing, and some parsing will fail. - Seller type.
DealerorPrivateSelleron AutoScout24,DEALERon CarGurus,PRIVATEon AutoTrader UK. Map them to one vocabulary. Keep the flag as a feature and let your data show whether it matters. - Deal ratings. AutoScout24 has
price.evaluation, CarGurusprice.rating, AutoTrader UKprice_indicator. They are separate fields with different values (1and2,NA,NOANALYSISin the recorded responses), and the responses don't say how each is computed. Don't pool them, and don't use one as your target if you're predicting price. - Localised text. The recorded AutoScout24
transmissionvalue isSchaltgetriebe, not "manual". Filter with the coded values from the taxonomy endpoint and normalise display text yourself.
One caveat applies everywhere: the price in each record is the advertised price as found at the time of the call. The recorded responses carry no sale-price field, and a negotiated sale can land away from the ask. If you need mileage you can trust, official records beat adverts: in the UK, the GOV.UK MOT history service shows the mileage recorded at each test. For decoding a CarGurus vin into factory specs, the NHTSA vPIC API decodes a VIN into key-value pairs.
How do you normalise three marketplaces into one table?
Write one small mapper per marketplace that outputs the same keys, and keep the currency as a column. The script below fetches one page from each source and prints a pooled table; the AutoTrader UK mileage stays None because the search record doesn't carry it.
import os
import requests
BASE = "https://api.perceptlayer.com"
HEADERS = {"X-API-Key": os.environ["API_KEY"]}
KM_PER_MILE = 1.609344
def listings(path, **params):
r = requests.get(BASE + path, headers=HEADERS, params=params, timeout=30)
r.raise_for_status()
return r.json()["data"]["listings"]
def from_autoscout24(l):
return {
"source": "autoscout24",
"currency": l["price"]["currency"],
"price": l["price"]["amount"],
"km": (l.get("mileage") or {}).get("value_km"),
"year": (l.get("first_registration") or {}).get("year"),
"trim": f'{l.get("variant", "")} {l.get("motor_type", "")}'.strip(),
"seller": "dealer" if l["seller"]["type"] == "Dealer" else "private",
"days_listed": None,
}
def from_cargurus(l):
m = l.get("mileage") or {}
km = m["value"] * KM_PER_MILE if m.get("unit") == "MILES" else None
return {
"source": "cargurus",
"currency": l["price"]["currency"],
"price": l["price"]["amount"],
"km": km,
"year": l.get("year"),
"trim": l.get("trim"),
"seller": l["seller_type"].lower(),
"days_listed": l.get("days_on_market"),
}
def from_autotrader_uk(l):
return {
"source": "autotrader-uk",
"currency": l["price"]["currency"],
"price": l["price"]["amount"],
"km": None,
"year": (l.get("tracking_context") or {}).get("year"),
"trim": l.get("sub_title"),
"seller": l["seller_type"].lower(),
"days_listed": None,
}
rows = [
*map(from_autoscout24, listings("/v1/autoscout24/search", market="de", make_id=13, model_id=1639, limit=20)),
*map(from_cargurus, listings("/v1/cargurus/search", make_model_trim_paths="m26/d126", distance=50000)),
*map(from_autotrader_uk, listings("/v1/autotrader-uk/search", postcode="SW1A1AA", price_search_type="total", make="BMW")),
]
for row in rows:
print(row)
That's 3 calls × 1 credit = 3 credits. Add page loops to build a real sample, and keep a collected_at timestamp on each row so repeat pulls become a price history.
What does a monthly price snapshot cost?
A repeatable snapshot is the pages in your sample times the number of pulls. Using the recorded AutoScout24 search, 193 listings at 20 per page is 10 calls. Pulled weekly for a year, that's 52 × 10 = 520 credits, which fits inside the free tier's 1,000 credits a month. Failed calls cost nothing.
Add a second marketplace and the arithmetic stays visible: 10 pages from each of three sources is 30 credits per snapshot. Credits come in prepaid packs that last 12 months; the pricing page has what credits cost, and credits in the docs explains debits. If a snapshot job runs unattended, read the rate limit headers and wait on a 429 for the Retry-After seconds.
Next step: get a free API key, run the AutoScout24 call above in the playground on its API page, and check which fields come back for the make you care about. For the same field-by-field approach on property data, see real estate listing data for valuation.
APIs in this post
Frequently asked questions
Which field matters most for predicting a used car's price?
Mileage and first-registration year are the first features to test, alongside make, model and trim; your own data will show their weights. In the recorded AutoScout24 results, a 316d Touring from a dealer (215,000 km, first registered 06/2013) asks EUR 5,700, while a 316d Limousine from a private seller (177,000 km, 01/2012) asks EUR 5,800. A 38,000 km gap moved the ask by only EUR 100 because body, seller and age differ too, which is why you test fields together.
Do these endpoints return the price a car actually sold for?
Not in the recorded responses. Each has one price field, price.amount, with no separate sale-price field, and data is returned as found at the time of the call. Treat listing data as a view of advertised levels, and confirm any sale-price need in the playground before you build around it.
How do I get listing age for AutoScout24 or AutoTrader UK?
The recorded search responses for those two don't include a listing-age field; CarGurus returns days_on_market. For the others you can sort by age (AutoScout24) or most_recent (AutoTrader UK) to order by recency, or record the first date you see each listing_id or advert_id in repeated pulls and compute age yourself.
Can I cover several countries with one key?
Yes. One key works across the whole catalogue, including these three marketplaces. AutoScout24 takes a market parameter with values de, at, be, it, fr, nl, es, com and pl. Each call costs 1 credit. Keep the currency and mileage unit per row, since they differ by market.
What happens if a call fails?
A failed call costs nothing and returns X-Credits-Used: 0. Errors come back as an HTTP status with a JSON body holding code, message and request_id, such as rate_limited (429) or invalid_parameters (422). A client timeout is the exception: the request may have succeeded and been billed. The errors docs list every code.