cargurus.ca scraper avatar

cargurus.ca scraper

Pricing

from $5.00 / 1,000 results

Go to Apify Store
cargurus.ca scraper

cargurus.ca scraper

This actor scrapes vehicle listings from CarGurus.ca search result pages. Pass any CarGurus.ca inventory search URL and get back structured JSON with listing details — price, mileage, year, make, model, dealer name, location, images, and CPO status.

Pricing

from $5.00 / 1,000 results

Rating

0.0

(0)

Developer

Mohamed Ibrahim

Mohamed Ibrahim

Maintained by Community

Actor stats

0

Bookmarked

15

Total users

0

Monthly active users

3 days ago

Last modified

Share

PRIVATE — internal cartell infrastructure. Not an Apify Store actor: no Store listing, no categories, no pay-per-event charging, no charging.ts. The docs/PUBLISHING.md and docs/LEGAL.md checklists in this repo govern the public @canadian-actors/* lane and do not apply here. See ../../docs/ARCHITECTURE.md → "Two actor lanes".

Provenance. Moved from the cartell repo at apify/cargurus-scraper/ (cartell@29094b8d). main.js, Dockerfile and .actor/*.json came across byte-identical; only package.json / package-lock.json name fields changed (rescoped to @cartell-actors/…, plus "private": true). The directory was renamed cargurus-scrapercargurus-ca-scraper so it matches the actor name, which is this repo's convention. Full history: git log -- apify/cargurus-scraper in cartell.

Scrapes vehicle listings from CarGurus.ca search result pages. Given any CarGurus.ca inventory search URL, it returns structured JSON — price, mileage, year, make, model, dealer name, location, images, CPO status.

Why an actor

CarGurus.ca is protected by DataDome bot detection. This actor gets through it with Chrome TLS fingerprinting via got-scraping plus Apify residential proxies — no headless browser, which keeps runs fast and cheap. Same bucket as AutoTrader (Incapsula), Nissan/Infiniti (Akamai) and Porsche (Vercel Attack Challenge Mode).

How it works

  • HTTP-only — no Playwright or Puppeteer overhead; a page fetches in seconds.
  • Session warm-up — establishes cookies on the homepage, then fetches search results as an established session.
  • Multi-strategy extraction — parses the embedded __PREFLIGHT__ JSON first, with DOM and alternative-endpoint fallbacks if the page structure rotates.
  • Pagination — ~24 listings per page; reads the total from CarGurus's metadata, fetches the pages needed to reach maxItems with randomized 1.5–2.5s delays, and dedupes by listing ID.

Input

FieldTypeRequiredDefaultDescription
searchUrlstringYesA CarGurus.ca inventory search URL
maxItemsintegerNo25Max listings to return (1–200). Fetches multiple pages if needed.
maxPagesintegerNo10Max pages to fetch (1–20). Each page adds ~2s of run time.
yearMinintegerNoFilter to model years >= this value
yearMaxintegerNoFilter to model years <= this value
debugbooleanNofalseEnable verbose logging for troubleshooting

To build a searchUrl: run the search you want on cargurus.ca with your filters (make, model, location, price range) and copy the URL from the address bar.

Example input

{
"searchUrl": "https://www.cargurus.ca/Cars/inventorylisting/viewDetailsFilterViewInventoryListing.action?zip=M5V%203A8&distance=50&entitySelectingHelper.selectedEntity=m7",
"maxItems": 100,
"maxPages": 5
}

Known gap: .actor/actor.json has no "input" field, so this input schema is very likely not registered on the platform (the no-input fallback looks for an uppercase INPUT_SCHEMA.json). Adding "input": "./input_schema.json" would start enforcing required: ["searchUrl"] and the numeric bounds on a live production path, so it is deliberately left alone here — fix it in its own PR with a verification run.

Output

One dataset item per listing:

{
"listingId": "432711846",
"listingUrl": "https://www.cargurus.ca/Cars/inventorylisting/viewDetailsFilterViewInventoryListing.action?#listing=432711846",
"listingTitle": "2022 Audi A4 45 TFSI quattro Progressiv S line AWD",
"year": 2022,
"make": "Audi",
"model": "A4",
"mileage": 34200,
"priceCAD": 38995,
"imageUrl": "https://static.cargurus.com/images/forsale/...",
"dealerName": "Toronto Auto Gallery",
"location": "Toronto, ON",
"isCertifiedUsed": false
}
FieldTypeDescription
listingIdstringCarGurus listing ID
listingUrlstringDirect link to the listing
listingTitlestringFull listing title (year, make, model, trim)
yearnumberModel year
makestringVehicle manufacturer
modelstringVehicle model
mileagenumberOdometer reading in km
priceCADnumberListed price in CAD
imageUrlstringPrimary listing photo URL
dealerNamestringDealer or seller name
locationstringCity/province of the listing
isCertifiedUsedbooleanWhether the vehicle is Certified Pre-Owned

Consumer-side mapping lives in cartell at lib/connectors/cargurus/client.ts, which also enforces the search radius (CarGurus reads distance in miles, so the connector re-filters). Keeping the actor extraction-free means a storefront schema rotation needs a parser change, not a redeploy.

Run cost

ScenarioRun timeApprox. cost
25 listings (1 page)5–15s~$0.01–0.03
50 listings (2–3 pages)10–25s~$0.02–0.05
100 listings (4–5 pages)20–40s~$0.04–0.08
200 listings (8–10 pages)40–70s~$0.08–0.15

Local development

cd actors/cargurus-ca-scraper
npm ci
APIFY_LOCAL_STORAGE_DIR=./storage npm start

Use npm, not pnpm — this actor is excluded from the pnpm workspace on purpose. An Apify account with RESIDENTIAL/CA proxy access is needed to actually clear DataDome.

Deploying

This actor has no build step, so it does not deploy via pnpm -F … push (that would silently match nothing). Either push a tag actor/cargurus-ca-scraper/v* to run ../../.github/workflows/apify-push.yml, or push manually:

cd actors/cargurus-ca-scraper
apify push

You must be logged in as the Apify account that already owns the live actor. A push from a different account silently creates a new actor under that account while cartell's CARGURUS_APIFY_ACTOR keeps pointing at the untouched original; a Created Actor with name … line in the push log is the failure signal.

cartell resolves this actor as CARGURUS_APIFY_ACTOR=<apify-username>~cargurus-ca-scraper. The .actor/actor.json name is therefore load-bearing — never rename it.

Maintenance notes

  • main.js is intentionally frozen: it is not linted or formatted by this repo's Biome config (see biome.json files.ignore), because reformatting a working anti-bot bypass is risk without benefit. Do not "clean it up".
  • Unlike the other two actors in this lane, the Dockerfile runs npm install, not npm ci — so the committed package-lock.json does not currently pin the deployed dependency tree. Switching it to npm ci is a real behaviour change to a working bypass; do it as its own PR with a live smoke run.
  • The Dockerfile also omits --chown=myuser on its COPY lines, which the current Apify base images expect. Pre-existing; same follow-up PR.
  • version: "0.0" + buildTag: "latest" means every push overwrites the live build. Treat any push as a production deploy.