Most people who search for a Carfax API want one of two very different things: the vehicle history report for a VIN, or the used car listings and dealer reviews anyone can browse on carfax.com. This repo covers the second one and refuses the first. It is a Carfax API client built on ScrapingBee for public, logged out carfax.com pages, tested on 2026-10-29. Read the decision table first; if your job is in the right hand column, the code below will not help you.
| Public carfax.com pages (this repo) | Vehicle History Report (out of scope) | |
|---|---|---|
| What it is | Used car search results, vehicle listing pages, dealer profiles and reviews | The paid per VIN report Carfax sells to consumers and businesses |
| Who can see it | Any visitor, no account | A buyer of the report, or someone a dealer shares it with |
| Example URL | /Used-Toyota-Camry_w630, /Reviews-<dealer>_<code> |
/vehiclehistory/... |
| History content | One line summaries printed on each listing card, such as "No Accident or Damage Reported" and "CARFAX 1-Owner" | Full ownership, title, odometer, accident and service records |
| Fetched by this code | Yes | Never. The listing JSON includes a report link (icrUrl); the parser drops it |
| ScrapingBee tier | premium proxy, US, no JavaScript: 10 credits a page | not attempted |
The summaries in the left column are part of the listing that the dealer advertises, and Carfax shows them to every visitor. The page footer says as much: "The CARFAX Vehicle History Report for the vehicle may contain more current information." If you need the report itself, buy it or use a licensed Carfax data product. Scraping it would mean getting past a paywall or reusing someone else's access, and this repo does neither.
| Page | Data | Where it sits in the HTML | Per request |
|---|---|---|---|
Search results, e.g. /Used-Honda-CR-V_w301?zip=10001&radius=50 |
24 listings with price, mileage, deal badge, history summaries, dealer name, rating and review count | window.__MOBX_STATE__ → SearchRequestStore.results.listings |
10 credits |
Make page, e.g. /Used-Toyota_m33 |
every model URL with a local listing count | SearchRequestStore.searchInfo.relatedLinks |
10 credits |
Vehicle page, /vehicle/<VIN> |
one listing in full: 101 feature strings on the Ram 2500 we opened, seller comments, fees, six line history snapshot | window.__MOBX_STATE__ → SearchRequestStore.selectedVehicle |
10 credits |
Dealer reviews, /Reviews-<name>_<code> |
rating split for sales and service, star percentages, the first 20 reviews | __NEXT_DATA__ → Apollo ROOT_QUERY |
10 credits |
| Settings | Credits | What came back |
|---|---|---|
mode=auto, max_cost=25 |
1 | HTTP 301 to https://www.carfax.eu/de, the German site. Auto mode saw a 200 and stopped |
premium_proxy=true, country_code=us, render_js=false |
10 | the US page with the full state object |
same plus render_js=true and a click on "Go to page 2" |
25 | still page 1; the only search related XHR, a POST to /api/ubs/search, got 403 |
direct call to the search backend helix.carfax.com/search/v2/vehicles |
0 | unbilled 500 both times: a DataDome 403 behind it on classic, an upstream 500 on premium |
The first row is the trap. Carfax redirects visitors outside the US, the redirect target answers 200, and Auto Mode counts any 200 as success. Check spb-resolved-url on every response; the client raises NotCarfaxUS when the host is not www.carfax.com. The blocked backend answered with a DataDome page:
<p id="cmsg">Please enable JS and disable any ad blocker</p>
<script data-cfasync="false">var dd={'rt':'c', ... 'host':'geo.captcha-delivery.com', ...}</script>Search pages are server rendered. Near the end of the HTML:
<script>window.__MOBX_STATE__= {"AppStore":{...},"SearchRequestStore":{...},"FilterStore":{...}}</script>Inside SearchRequestStore, the pagination block and one listing, trimmed:
"pagination": {"pageNum": 1, "totalPages": 29, "totalRecords": 678, "pageSize": 24, "displaying": "1 - 24"},
"results": {"listings": [{
"vin": "3C63R5DL1SG522873", "year": 2025, "make": "Ram", "model": "2500", "trim": "Big Horn",
"badge": "GREAT", "mileage": {"label": "22,726", "value": 22726},
"listPrice": 52527, "currentPrice": 52527,
"onePrice": {"label": "$57,110", "difference": "$4,583 below"},
"pillars": {
"accidentHistory": {"text": "No Accident or Damage Reported", ...},
"ownerHistory": {"text": "CARFAX 1-Owner", ...},
"vehicleUseHistory": {"text": "Personal Use", ...},
"serviceHistory": {"text": "Service History", "number": 8, ...}},
"dealer": {"label": "Geaux Chevrolet West", "phone": "(225) 425-3830",
"dealerAverageRating": 4.8, "dealerReviewCount": 95,
"url": "https://www.carfax.com/Reviews-Geaux-Chevrolet-West-Plaquemine-LA_VJRQHOMPSQ", ...},
"icrUrl": "https://www.carfax.com/vehiclehistory/ccl/...", ...}]}The same store also records which backend rendered the page. searchRequest.apiUrl points at helix-web.internal.us-east-1.k8s.cxd.production.aws.carfax.io/search/v2/vehicles?make=Honda&model=CR-V&zip=10001&radius=50&vehicleCondition=USED..., an internal host the browser never calls.
Reading the object takes json.JSONDecoder().raw_decode, which stops at the closing brace and ignores the script that follows:
m = re.search(r"window\.__MOBX_STATE__\s*=\s*", html)
mobx, _ = json.JSONDecoder().raw_decode(html[m.end():])
listings = mobx["SearchRequestStore"]["results"]["listings"]export SCRAPINGBEE_API_KEY=...
python examples/carfax_client.py listings /Used-Toyota-Camry_w630 --zip 10001 --csv camry.csv --json camry.json zip ignored (got 'Nationwide'), asking again
zip ignored (got 'Nationwide'), asking again
678 listings in New York, NY 10001 +50mi, page 1 holds 24, sort BEST, 30 credits
2024 Toyota Camry LE $ 23,182 32,500 mi GOOD No Accident or Damage Reported Empire Nissan of Hillside
2025 Toyota Camry SE $ 30,037 30,360 mi GREAT No Accident or Damage Reported Motion Kia
2023 Toyota Camry LE $ 22,170 27,756 mi GREAT No Accident or Damage Reported Kia City
2020 Toyota Camry SE $ 21,995 31,168 mi GREAT No Accident or Damage Reported Sunrise Toyota
That run shows the most annoying behaviour we found. Carfax sometimes ignores the zip parameter and answers "Nationwide" with a different result set (19,521 Camrys instead of 678). It happened on 4 of 13 zip requests during testing, with identical URLs. search() compares searchArea.zip with what you asked for and repeats the request up to two times, so a bad roll costs 10 more credits.
Across those 24 New York Camrys: 18 carried a GREAT badge and 6 GOOD; 18 said "No Accident or Damage Reported", 4 "Minor Damage", one "Accident/Minor Damage" and one "Accident Reported"; 21 were one owner cars.
The number after the underscore is what Carfax routes on. /Used-Toyota_m31 redirected to /Used-Scion_m31, and the zip was lost on the way. Pull the right paths from a make page instead of guessing:
$ python examples/carfax_client.py models /Used-Toyota_m33 --zip 60601
120 Used Toyota 4Runner https://www.carfax.com/Used-Toyota-4Runner_w628
32 Used Toyota Avalon https://www.carfax.com/Used-Toyota-Avalon_w629
447 Used Toyota Camry https://www.carfax.com/Used-Toyota-Camry_w630
372 Used Toyota Corolla https://www.carfax.com/Used-Toyota-Corolla_w631
Each search URL gives you its first 24 listings and no more. ?page=2 is ignored on the server side, clicking the page 2 button in a rendered session left the page on page 1, and calling the search backend from inside the page got the DataDome 403 shown above. ?sort=PRICE_ASC was worse: Carfax dropped the zip and fell back to its default sort. The way to cover more cars is more URLs, not more pages: other zips, other models, trims and model years (the breadcrumb on a vehicle page links paths like /Used-2025-Ram-2500_z40121 and /Used-2025-Ram-2500-Big-Horn_x55142), and the price bands from /cars-for-sale such as /Used-Cars-Under-10000_f6.
The pagination block is honest about this in its own way. For Toyota around New York it reported totalRecords: 4752 and totalPages: 50, which at 24 a page would only be 1,200 cars.
Every listing card carries dealerAverageRating and dealerReviewCount. The dealer page adds the split between sales and service, the star distribution, and the review text. It is a Next.js page, so the data is in __NEXT_DATA__ as Apollo cache keys:
"shop({\"compCode\":\"JVQRHOMPSQ\"})": {
"companyName": "Geaux Chevrolet West", "dealerBadge": "TOP_RATED_LIFETIME",
"ratingsByType": {
"all": {"averageRating": 4.8, "numberOfReviews": 95, "ratingPercentages": [91, 5, 0, 1, 3]},
"sales": {"averageRating": 4.8, "numberOfReviews": 5, "ratingPercentages": [80, 20, 0, 0, 0]},
"shop": {"averageRating": 4.8, "numberOfReviews": 90, "ratingPercentages": [91, 5, 0, 1, 3]}}},
"reviews({\"compCode\":\"JVQRHOMPSQ\",\"reviewParameters\":{\"paginationParameters\":{\"page\":1,\"rows\":20},...}})": {
"paginationDetails": {"page": 1, "row": 20, "totalRecords": 95, "totalPages": 5},
"items": [{"date": "2026-10-22T16:08:46", "rating": 5, "type": "SALES_REVIEW",
"username": "2023 GMC CANYON AT4 Shopper", "badge": "Verified Purchase", ...}]}ratingPercentages runs from five stars down to one. A JSON-LD block on the same page repeats the headline numbers ("@type": "AutomotiveBusiness", "ratingValue": 4.8, "reviewCount": 95), which is a handy cross check.
The page holds the first 20 reviews in Carfax's "best match" order. ?page=2 and ?reviewPage=2 both came back with the same 20. For a dealer with 95 reviews you get the full star distribution and a fifth of the text.
$ python examples/carfax_client.py dealer https://www.carfax.com/Reviews-Geaux-Chevrolet-West-Plaquemine-LA_VJRQHOMPSQ --csv reviews.csv
{
"code": "JVQRHOMPSQ",
"name": "Geaux Chevrolet West",
...
"badge": "TOP_RATED_LIFETIME",
"rating_all": 4.8,
"reviews_all": 95,
"rating_sales": 4.8,
"reviews_sales": 5,
"rating_service": 4.8,
"reviews_service": 90,
"stars_5_to_1_pct": "91/5/0/1/3",
...
}
2026-05-02 4* SHOP_REVIEW Service is quick & people are nice, but the tech who changes oil & rot
2026-10-22 5* SALES_REVIEW We have felt with Brad Hanks for years. Glad to see he is back in the
20 reviews read, 10 credits
examples/carfax_dealers.py chains the two: it reads a search page, groups the cars by dealer, then opens the review pages of the busiest dealers.
$ python examples/carfax_dealers.py /Used-Honda-CR-V_w301 --zip 10001 --dealers 3
1704 listings in New York, NY 10001 +50mi; scoring dealers from the 24 on page 1
Lia Honda Brewster NY 4 cars median $25,656 rating 4.6 (857) 7% one or two stars
Bay Ridge Honda 3 cars median $29,470 rating 4.6 (882) 6% one or two stars
North Shore Honda 2 cars median $26,495 rating 4.8 (1121) 4% one or two stars
Route 7 Nissan 1 cars median $29,965 rating 4.7 (32) review page not opened
Empire Mazda of Huntington 1 cars median $9,675 rating 4.3 (343) review page not opened
18 dealers, 60 reviews, 40 credits
examples/carfax_listings.js prints one JSON line per listing. It cuts the state object out by counting braces, because the script tag continues after it:
$ node examples/carfax_listings.js /Used-Tesla-Model-3_w9421 94103 > tesla.jsonl
10 credits, https://www.carfax.com/Used-Tesla-Model-3_w9421?zip=94103&radius=50
189 listings, area: San Francisco 94103
{"vin":"5YJ3E1EA0PF612071","title":"2023 Tesla Model 3 Standard Range","price":22084,"mileage":111997,"badge":"GREAT","accidents":"Accident/Minor Damage","owners":"CARFAX 1-Owner","service_records":3,"dealer":"One Toyota of Oakland","dealer_rating":4.4}
| Job | Credits |
|---|---|
| 24 listings for one model and zip | 10, or 20 to 30 when the zip is ignored |
| Model list for a make | 10 |
| One dealer: rating split and 20 reviews | 10 |
| Scorecard: one search and three dealers | 40 |
New accounts get 1,000 free credits, so about 100 Carfax pages. Plans are on the pricing page. Parameter reference: HTML API documentation, JavaScript scenarios, data extraction.
Logged out pages only. Saved cars, followed listings, price alerts and the garage need a Carfax account and are not touched; ScrapingBee's terms of service do not allow scraping under login credentials, and this repo does not try. Review authors appear as a vehicle ("2018 HONDA PILOT EXL Owner") or a first name and initial, which is still personal data in some jurisdictions, so store it only if you need it. Carfax's own terms of use apply to the site.
Related endpoints: Copart API, CarGurus API, Cars.com API, Autotrader API, vehicle data API, Google reviews API.
Can this pull a Carfax report by VIN?
No. The report is a paid product and stays out of scope. For a VIN that is currently listed for sale you get the public listing at /vehicle/<VIN>, with the history snapshot the dealer shows: accident line, owner count, service record count, last reported odometer.
Is there an official Carfax API for developers? Carfax sells data services to businesses under contract. This repo does not use them and makes no claims about them; ask Carfax if your use needs licensed history data.
How do I scrape Carfax reviews for a dealer?
Take the dealer.url from any listing, or the /Reviews-..._<code> URL from the dealer directory, and run carfax_client.py dealer <url>. You get averages for sales and service, the star percentages and the first 20 reviews.
Why did my Carfax scraper return a German page?
Your request left from outside the US. Carfax redirects those to carfax.eu. Use premium_proxy=true with country_code=us and check spb-resolved-url.
How many listings can I collect per search? 24 per URL. Spread the job across zips, models, years and price bands; each URL costs 10 credits.
MIT. See LICENSE.