DEV Community

Roberto Kerber
Roberto Kerber

Posted on

How to Scrape Webmotors: Extract Brazilian Used Car Listings, Prices & Specs (2026)

If you are trying to scrape Webmotors - Brazil's largest used car marketplace - to monitor prices, benchmark a dealership's stock, or feed a market research dashboard, you already know the frustrating part: it is not a static HTML page you can pull with a single requests.get(). This post walks through why scraping Webmotors is harder than it looks, what a DIY approach actually costs you in maintenance, and how to get clean structured data (price, odometer, seller, specs, FIPE percentage) without babysitting a scraper. Whether you searched "webmotors scraper", "webmotors api" or "scrape used cars brazil" to get here, the tradeoffs below apply either way.

Why scraping Webmotors is harder than it looks

Webmotors, like most modern car marketplaces, renders its listing grid client-side with JavaScript. A plain requests.get() returns a near-empty HTML shell - the actual make, model, price and photos get injected after the page hydrates in the browser. That alone rules out the simplest scraping approach and forces you into a headless browser.

Then there is anti-bot protection. Car marketplaces get scraped constantly by dealers, aggregators and price-comparison tools, so datacenter IPs get rate-limited or blocked outright after a handful of requests. You either rotate residential proxies yourself, or your scraper quietly dies after a few hundred listings.

On top of that:

  • Pagination is not always a simple ?page=2 - it can involve infinite scroll, background API calls, and cursor-based paging
  • Price parsing - listings show "R$ 89.900" as a formatted string, not a usable number
  • Layout drift - the DOM changes whenever the marketplace ships a redesign, which quietly breaks CSS selectors and hands you empty or wrong fields with no error thrown

None of these problems is hard on its own. Together, they are why "just scrape it" turns into a part-time maintenance job.

Approach 1: DIY with Python

Attempt 1: plain requests (does not work)

import requests

r = requests.get("https://www.webmotors.com.br/carros/estoque?query=honda+civic")
print(r.status_code, len(r.text))
# 200, but the HTML body is a near-empty app shell.
# Listing data isn't in the initial response - it's fetched
# and rendered client-side after the page loads.
Enter fullscreen mode Exit fullscreen mode

This is the first wall everyone hits: the response comes back 200 OK, but there is no listing data in it. You need a real browser.

Attempt 2: headless browser with Playwright

from playwright.sync_api import sync_playwright

def scrape_civic(max_ads=50):
    listings = []
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto("https://www.webmotors.com.br/carros/estoque?query=honda+civic")
        page.wait_for_selector(".ResultCard", timeout=15000)

        cards = page.query_selector_all(".ResultCard")
        for card in cards[:max_ads]:
            title = card.query_selector(".ResultCardTitle")
            price_raw = card.query_selector(".ResultCardPrice")
            listings.append({
                "title": title.inner_text() if title else None,
                "price_raw": price_raw.inner_text() if price_raw else None,
            })
        browser.close()
    return listings

def parse_price(price_raw: str) -> int:
    # "R$ 89.900" -> 89900
    digits = "".join(c for c in price_raw if c.isdigit())
    return int(digits) if digits else 0
Enter fullscreen mode Exit fullscreen mode

This works locally, on your own IP, for a handful of requests. Selector names above are illustrative - the real ones you'll find in the page will differ, and that's exactly the point: the moment you scale this past a few dozen pages or run it on a schedule from a server, requests start getting blocked or CAPTCHA-walled, the selectors break the next time the site ships a frontend update, and you are now responsible for proxy rotation, retries and selector maintenance - forever, not once.

The real cost of DIY isn't writing the scraper, it's keeping it alive

A one-off script for 20 listings is genuinely fine to write yourself, no argument there. The cost shows up when you need this reliably, on a schedule, at volume:

DIY (Playwright + your own infra) Managed scraper (API)
Upfront cost Free (your time) Pay per listing returned
JS rendering You maintain the headless browser Handled for you
IP blocking You buy and rotate residential proxies Already routed through a residential IP
Selector breakage You fix it every time the site changes Maintained on the provider's side
Price/field parsing You write and maintain it Comes back as typed JSON (price: 89900, not a string)
Scheduling You wire up cron + monitoring Native scheduler on the platform

Neither column is objectively "right." If you need ten listings once for a one-off report, write the Playwright script above and move on. If you need this running daily, at hundreds or thousands of listings, the maintenance tax is the real cost, not the code.

Approach 2: a ready-made Webmotors scraper (API)

This is the part where I show you the shortcut. Webmotors Scraper is an Apify actor that handles the JS rendering, residential IP routing, pagination and price parsing described above, and returns structured JSON per listing.

Input

Two fields, no proxy configuration:

Field Type Description Example
query string Keyword to search on Webmotors "honda civic"
maxAds number Maximum number of listings to return 100
{
  "query": "honda civic",
  "maxAds": 100
}
Enter fullscreen mode Exit fullscreen mode

Output

Every listing comes back flat, typed and ready to sort, filter or insert into a database - no nested objects, no string-to-number parsing:

{
  "uniqueId": 71834512,
  "title": "HONDA CIVIC 2.0 16V FLEXONE EXL 4P CVT",
  "make": "HONDA",
  "model": "CIVIC",
  "version": "2.0 16V FLEXONE EXL 4P CVT",
  "yearFabrication": 2023,
  "yearModel": 2023,
  "odometer": 34000,
  "transmission": "Automática",
  "bodyType": "Sedan",
  "color": "Preto",
  "price": 139900,
  "sellerType": "PJ",
  "sellerName": "AUTO PRIME",
  "city": "Sao Paulo",
  "state": "Sao Paulo (SP)",
  "neighborhood": "Moema",
  "fipePercent": 98,
  "listingType": "U",
  "imageCount": 12,
  "thumbnail": "https://image.webmotors.com.br/_fotos/anunciousados/gigante/2026/.../honda-civic-wmimagem123.jpg",
  "url": "https://www.webmotors.com.br/carros/honda/civic/71834512/"
}
Enter fullscreen mode Exit fullscreen mode

price and odometer come back as integers, not "R$ 89.900" strings, so df["price"].median() or SELECT AVG(price) just works. fipePercent (asking price as a percentage of the FIPE reference table) is worth noticing on its own: it is a ready-made signal for underpriced listings, which is the whole basis for the arbitrage use case below.

Calling it from JavaScript

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });

const run = await client.actor('plum_spear/aztec-webmotors').call({
  query: 'honda civic',
  maxAds: 100,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, 'listings');
console.log(items[0]);
Enter fullscreen mode Exit fullscreen mode

Calling it from Python

from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("plum_spear/aztec-webmotors").call(run_input={
    "query": "honda civic",
    "maxAds": 100,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item["price"], item["fipePercent"])
Enter fullscreen mode Exit fullscreen mode

Calling it from the CLI or plain REST

apify call plum_spear/aztec-webmotors --input '{"query": "honda civic", "maxAds": 100}'
Enter fullscreen mode Exit fullscreen mode
curl "https://api.apify.com/v2/acts/plum_spear~aztec-webmotors/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"query": "honda civic", "maxAds": 100}'
Enter fullscreen mode Exit fullscreen mode

What people actually build with this

These map directly to how the data gets used in practice, not hypothetical scenarios:

  • Price monitoring and repricing. Run the same query daily on honda civic or toyota corolla and track how the price distribution moves. Filter on fipePercent < 90 to flag listings priced meaningfully below the FIPE reference table.
  • Dealership competitive intelligence. Pull every listing for the models your dealership stocks and benchmark your price against the live market, by state and mileage band.
  • Market research and segment analysis. Pull a broad query (suv, pickup, sedan) and compute median price per brand for a report, instead of eyeballing a handful of listing pages.
  • Car buying research. Before a purchase, pull the model and year you're evaluating and see the full price spread across Brazil before you negotiate.
  • Arbitrage and reselling. Diff new listings by uniqueId on a frequent schedule and act on underpriced vehicles before anyone else refreshes the page.

Pricing

Pay-per-event: $0.15 per 1,000 listings returned, plus a minimal actor-start event. No subscription, no monthly minimum - you pay for the data you pull, not for idle time. Apify gives new accounts free monthly platform credits, so you can run a real query against your own use case before deciding whether it's worth it.

For context: if the DIY route above costs you a residential proxy subscription plus a few hours a month keeping selectors alive, $0.15 per 1,000 listings is the kind of number that stops being a debate past a certain volume. Below that volume, write the script above - it's genuinely fine.

Using it from an AI agent (MCP)

If you're wiring this into an agent instead of a script, actors published on Apify, including this one, are reachable through Apify's MCP server, which exposes them as callable tools for MCP-compatible clients. Same query / maxAds input, no separate integration to write.

Wrap-up

Scraping Webmotors yourself is doable, and for a one-off pull it's probably the right call - the Playwright snippet above will get you there. What it won't do on its own is survive a layout change, dodge IP blocking at scale, or hand you typed data you can pipe straight into a spreadsheet or database. That gap is what Webmotors Scraper on Apify closes: give it a query and a maxAds, get back clean JSON with parsed prices, FIPE comparison and seller details, priced at $0.15 per 1,000 listings with no monthly commitment.

Top comments (0)