DEV Community

Devil Scrapes
Devil Scrapes

Posted on

AutoTrader.ca Listings Scraper: when an HTTP 200 is a block, not an empty search

Quick answer

The AutoTrader.ca Listings Scraper pulls Canadian car listings by make, model and condition into JSON, CSV or Excel: price in CAD, mileage in kilometres, year, trim, body type, transmission, fuel, seller name and type, city, province, photos and the full seller description. One run covers the whole country, up to 4,000 listings per make, model and condition. AutoTrader.ca has no public listings API and actively blocks automated traffic, so we run it through residential exits and treat every kind of soft block as a block. Pricing is pay-per-result, $0.20 per run plus $0.0025 per listing, $2.70 per 1,000 listings.

Does AutoTrader.ca have an API?

Not for the public. Dealer feeds go in through partner integrations, and nothing comes out. If you want "every used Toyota Corolla for sale in Canada with price and mileage", the only source is the search pages themselves. Each page is a Next.js render with 20 listings inside a __NEXT_DATA__ script tag, along with the total result count and the number of pages.

When is an HTTP 200 not a page of listings?

When the site has decided you are a bot. AutoTrader.ca's protection doesn't always answer with a clean 403. Sometimes it returns HTTP 202 with a challenge body. Sometimes it returns HTTP 200 with a page that has no listing data in it at all. A scraper that only checks the status code reads both as "this search has no cars" and finishes green with an empty dataset.

On AutoTrader.ca, a 202 or a 200 without __NEXT_DATA__ is a block, not an empty search.

So we treat all three as blocks: a non-200, a 202, and a 200 that lacks the listing payload. Each one triggers a retry on a fresh residential session. The only "zero results" we believe is a page that parses cleanly and reports a total of zero. If a page still fails after five attempts we log it, keep every other page, and end the run with a status like "Scraped 380/400 results; 1 page blocked", so you can see exactly what you got.

What else we clean up

  • Numbers become numbers. "$ 13,990" becomes price_cad: 13990, and the odometer becomes mileage_km, while price_formatted keeps the original string.
  • Descriptions become plain text. Seller HTML with line breaks and entities comes back as readable text with paragraph breaks kept.
  • Make and model are forgiving. "Land Rover", "land rover" and "land_rover" all work.
  • Duplicates are dropped. A listing that shows up on two pages is emitted once: we dedupe on listing_id, and result_type tells you whether it was Organic or Sponsored.
  • The pages are heavy. Each search page weighs roughly 0.78 MB for 20 listings, and that egress is part of the price you see.

What the Actor gives you

One row per listing, 29 fields, including listing_id, url, title, year, make, model, trim, body_type, price_cad, price_formatted, price_evaluation, mileage_km, transmission, fuel, engine_cc, condition, city, province, postal_code, street, seller_name, seller_type, seller_phone, ad_tier, result_type, image_urls, image_count, description and scraped_at.

{
  "listing_id": "79df9408-002a-44d1-90a3-3355eb9b1770",
  "url": "https://www.autotrader.ca/offers/toyota-corolla-le-79df9408-002a-44d1-90a3-3355eb9b1770",
  "title": "2018 Toyota Corolla LE",
  "year": 2018,
  "make": "Toyota",
  "model": "Corolla",
  "trim": "LE",
  "body_type": "Sedan",
  "price_cad": 13990,
  "price_formatted": "$ 13,990",
  "mileage_km": 144425,
  "transmission": "Automatic",
  "fuel": "Gasoline",
  "condition": "used",
  "city": "North York",
  "province": "ON",
  "seller_name": "First Choice Motors",
  "seller_type": "Dealer",
  "result_type": "Organic",
  "image_count": 36,
  "scraped_at": "2026-10-07T12:00:00+00:00"
}
Enter fullscreen mode Exit fullscreen mode

Input: make, model, condition (used or new) and maxResults up to 4,000.

Honest limitations 🚧

Search is by make, model and condition only: no postal-code radius, province, price, year or mileage filter, so filter the dataset afterwards. One search returns at most 4,000 listings (200 pages of 20). Listing pages only: no VIN, full spec sheet, price history or sold listings. Phone numbers appear only when the listing itself shows one.

FAQ

Do I need an AutoTrader.ca account?
No. The Actor reads public search pages.

What about blocks and captchas?
That's our job. We rotate real-browser TLS fingerprints through curl-cffi, route through Apify residential proxies with a fresh session on every block, and retry with exponential backoff up to 5 attempts per page, honouring Retry-After. One blocked page never sinks the run.

Can I track prices for one model over time?
Yes. Schedule the same make and model daily and diff price_cad per listing_id.

How does pricing work?
$0.20 per run plus $0.0025 per listing: $2.70 per 1,000 listings.

→ AutoTrader.ca Listings Scraper on Apify


Built by Devil Scrapes. We handle the residential proxies, the fingerprints, the soft blocks and the retries, so an empty dataset always means an empty search. 😈

Top comments (1)

Collapse
 
ispyhumanfly profile image
Dan Stephenson •

The HTTP 200 trap on AutoTrader is a well-chosen edge case to document — returning a "successful" empty page instead of a 4xx is a deliberate anti-scraping pattern that breaks naive retry logic entirely. For anyone maintaining Actors like this, there's a public breakdown of how result volume maps to revenue under different models at bytewells.com/apify-rental-alterna... that's worth a look. How do you handle cases where AutoTrader returns that 200-but-blocked response mid-pagination rather than on the first request? #thoughtpivot