For-sale search returns listings you can actually buy: a VIN, a price, mileage, and a dealer listing id. You search by make, model, and ZIP. The make and model are names, not the numeric ids used by the trims endpoint.
👉 To access this endpoint, you must obtain an API key: https://steadyapi.com/register
Authentication
Every call uses a Bearer token from your SteadyAPI dashboard.
Documentation:
https://docs.steadyapi.com/#authenticating-requests
headers = {
'Authorization': 'Bearer YOUR_API_KEY'
}
Where this fits
Resolve the make and model names first if a user typed a fragment, then call GET /v1/vehicles/for-sale. A listing’s year, make, and model can go to GET /v1/vehicles/details. The listing id is not a vehicleId for car value or recalls.
Find vehicles for sale
Endpoint: GET /v1/vehicles/for-sale
Documentation:
https://docs.steadyapi.com/#autohub-api-GETapi-v1-vehicles-for-sale
Parameters
make— required, max 50 characters (example:Mazda)model— required, max 50 characters (example:CX-5)zipcode— required, max 10 characters (example:73170)year— optional, 4 characterssource— optional, defaultkbbradius— optional miles, default50, max200min_price,max_price,min_mileage,max_mileage— optional integerspage— optional, default1per_page— optional, default20, max50sort_by— optional:relevance,price,mileage,year,distance,date_listed. Defaultrelevancesort_order— optionalascordesc, defaultasc
Make and model are uppercased before the search. meta.make in the sample is MAZDA. Zip code is required here, unlike car value, which defaults to 73101. Omit source and the lookup uses kbb. The sample below is a response whose meta.source is carmax, so that call set source. Radius, price, and mileage filters change which listings return. They do not change the shape of a row.
Python
import requests
url = "https://api.steadyapi.com/v1/vehicles/for-sale"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
response = requests.get(url, headers=headers, params={
"make": "Mazda",
"model": "CX-5",
"zipcode": "73170",
"source": "carmax",
})
response.raise_for_status()
payload = response.json()
meta = payload.get("meta") or {}
print(meta.get("total_listings"), meta.get("total_pages"))
for car in (payload.get("body") or [])[:5]:
print(car.get("vin"), car.get("year"), car.get("price"), car.get("mileage"))
Annotated sample
{
"id": 28135196,
"vin": "JM3KFACM7L0805890",
"year": 2020,
"make": "Mazda",
"model": "CX-5",
"trim": "Touring",
"price": 22998,
"mileage": 31522,
"listing_type": "USED",
"is_new": false
}
Store vin and the listing id. price and mileage are numbers. The sample meta reports total_listings 558 and total_pages 28 for that search, with a price range from 12998 to 28998. images is a list of objects with a url. days_on_market can be null.
Practical use
Page through with page until you pass meta.total_pages. per_page above 50 is rejected. The default sort is relevance ascending, not lowest price.
A 2020 Touring at 31522 miles is one listing. The next row can be a different year. If you passed year, still read year on the row before you display it.
meta.source tells you which inventory this page came from. A kbb page and a carmax page for the same make and ZIP are not the same list.
👉 Get your API key and find listings: https://steadyapi.com/register