By the end of this tutorial, you will have a Python script that turns a webmotors.com.br car search into structured JSON: one row per listing with the asking price in Brazilian reais, fabrication and model year, mileage in kilometres, transmission, body type, colour, engine, city, state, and the dealership's trade name when a dealer is selling.
Webmotors is a Brazilian vehicle classifieds site. Its search endpoint serves 24 cars per page by default, and in October 2026 testing it answered direct requests, a datacenter proxy and a residential proxy with a PerimeterX challenge page (HTTP 403) instead of results.
We will use Python and the Apify Actor Webmotors Brazil Scraper (reventadata/webmotors-scraper), which reads the site through Apify's Unblocker proxy by default. The sample row in this guide is a sample from a capture dated 2026-10-17, parsed with the Actor's own code.
⏱️ Time estimate: one sitting: install one package, set one token, run three short Python blocks.
Prerequisites
To follow this tutorial, you will need:
-
Python 3.11+ installed locally (
apify-client3.x requires it). - The Apify API client for Python.
- An Apify account and API token.
Sign up for an Apify account, then copy your API token from the Integrations tab in Apify Console. Install the client:
pip install "apify-client~=3.0"
The code loads the token from an environment variable:
export APIFY_API_TOKEN="your_apify_token_here"
What you'll achieve
You will turn a Webmotors search into rows you can load into a spreadsheet or database: listing id and link, title, make, model and version, fabrication and model year, price, odometer in kilometres, new or used, transmission, colour, doors, body type, engine size, horsepower, seller type, dealer name, city, state, a photo URL and the listing's feature tags (such as "Único dono", single owner).
Note on data boundaries: The Actor reads the site's search results only, never listing detail pages. Private sellers get no name in the row: the dealer name is filled only for dealership listings.
Example use case: You want Porsche 911 listings across Brazil with their prices and mileage. Setting searchQueries: ["porsche 911"] asks the site for that make and model, and each row tells you the year, the kilometres and which dealership is selling.
Step 1: Access Webmotors Brazil Scraper
Go to the Webmotors Brazil Scraper page on Apify Store to try it.
In this tutorial we start it from Python, so the same script can run again tomorrow.
Step 2: Configure the search
The main input is searchQueries, a list of make and model searches. The Actor turns each one into a Webmotors search path: the first word becomes the make and the rest becomes the model, so "porsche 911" becomes /carros/estoque/porsche/911. A one-word query is read as a make ("honda" lists every Honda); a model alone such as "civic" is skipped with a warning.
In Python, we create the client and the input:
import os
from apify_client import ApifyClient
# 1. Initialize ApifyClient with token from environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
# 2. Define the search: Porsche 911 listings, up to 20 rows
run_input = {
"searchQueries": ["porsche 911"],
"maxItems": 20,
}
(Note: the sample row in this guide is one Porsche 911 listing captured from Webmotors on 2026-10-17 and parsed with the Actor's code. It is not the output of a run of this input.)
Step 3: Optional settings and filters
Every filter below goes into the Webmotors search URL, and the Actor checks each row against it again before saving it.
| Parameter | Type | Default | Description |
|---|---|---|---|
searchQueries |
array | ["honda civic"] |
Make and model searches, first word = make. |
maxItems |
integer | 100 |
Total listings to save, across all queries. |
condition |
string | "all" |
all, used, new. |
minYear / maxYear
|
integer | empty | Year range (model year, or fabrication year when there is none). |
minPrice / maxPrice
|
integer | empty | Price range in reais. |
maxMileageKm |
integer | empty | Odometer cap in kilometres. |
state |
string | empty | Two-letter state code, such as SP or RJ. |
transmission |
string | "all" |
all, automatic, manual. |
sortBy |
string | "relevance" |
relevance, lowest_price, highest_price, lowest_km, newest_year. |
requireKeywordMatch |
boolean | false |
Keep only rows whose title holds every query word. |
A few practical notes:
-
The fixed lists are checked before the run starts. A value outside
condition,transmissionorsortByis refused by the platform and the Actor is not started.stateis free text, so type the code carefully. -
maxItemsis shared by all queries. The first query can use the whole budget; list the most important search first.
Step 4: Run the Actor
Start the run and wait for it with client.actor("reventadata/webmotors-scraper").call(run_input=run_input):
# 3. Start the Actor run and wait for it to complete
print("Starting Webmotors extraction...")
run = client.actor("reventadata/webmotors-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
status = run.status if run else "unknown"
raise SystemExit(f"Run did not succeed (status: {status}).")
# 4. Fetch the resulting dataset items
dataset = client.dataset(run.default_dataset_id).list_items().items
print(f"Extraction complete! Retrieved {len(dataset)} listings.\n")
Behind the scenes:
- The Actor sends each search to the site's own search endpoint through Apify's Unblocker proxy (the default). In the October 2026 test, Unblocker was the one connection that got results; no proxy, datacenter and residential all got the challenge page.
- It asks for 24 cars per page and reads pages in order until it has
maxItemsrows or a page comes back with fewer than 24 cars. - A listing id it has already saved is skipped, also across queries.
Step 5: View and download results
Once the run finishes, print a short summary of each row:
for item in dataset:
price = item.get("price")
km = item.get("odometerKm")
mileage = f"{km:,} km" if km is not None else "not stated"
print(f"Listing: {item.get('title')}")
if not price:
print(" Price: not listed")
else:
print(f" Price: R$ {price:,.0f}")
print(f" Year: {item.get('yearFabrication')}/{item.get('yearModel')}")
print(f" Mileage: {mileage}")
print(f" Seller: {item.get('sellerType')} ({item.get('dealerName') or 'no dealer name'})")
print(f" Location: {item.get('city')}, {item.get('state')}")
print("-" * 50)
Apify datasets also export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Here is one row, a sample from a capture dated 2026-10-17, parsed with the Actor's own code. A saved row also carries scrapedAt, added at save time:
{
"itemId": "78266189",
"title": "PORSCHE 911 4.0 24V H6 GASOLINA GT3 MANUAL",
"url": "https://www.webmotors.com.br/comprar/porsche/911/porsche-911-40-24v-h6-gasolina-gt3-manual/78266189",
"price": 2399000.0,
"make": "PORSCHE",
"model": "911",
"version": "4.0 24V H6 GASOLINA GT3 MANUAL",
"yearFabrication": 2025,
"yearModel": 2026,
"odometerKm": 1563,
"condition": "used",
"transmission": "Manual",
"color": "Prata",
"doors": "2",
"bodyType": "Cupê",
"engineSize": "4.0",
"horsePower": "510 cv",
"sellerType": "dealership",
"dealerName": "STUTTGART PORSCHE - BLUMENAU",
"city": "Blumenau",
"state": "Santa Catarina (SC)",
"imageUrl": "https://image.webmotors.com.br/_fotos/AnuncioUsados/div/2026/202609/20260924/porsche-911-4-0-24v-h6-gasolina-gt3-manual-wmimagem15471201988.webp",
"attributes": [
"Aceita troca",
"Único dono",
"IPVA pago",
"Licenciado"
],
"matchedKeyword": "porsche 911"
}
Understanding your results
The key fields:
-
itemId: the Webmotors listing id.urlis a link the Actor builds from make, model, title and that id. -
title,make,model,version: as the site writes them (in capitals, in the sample). -
yearFabrication/yearModel: Webmotors gives both. The sample car was built in 2025 as a 2026 model. -
price: the asking price in reais, as a number. It is0.0when the listing has no numeric price. -
odometerKm: kilometres. The sample shows 1,563 km on a car listed asused. -
condition:usedornew, from the listing type. -
transmission,bodyType,color: in Portuguese (Cupêis coupé,Pratais silver). -
sellerType/dealerName:dealershiporprivate; the trade name only for dealerships. -
attributes: the listing's feature tags. "Aceita troca" means the seller accepts a trade-in, "IPVA pago" that the annual vehicle tax is paid. -
matchedKeyword: the query that found the row.
Pro tips
- Write queries as make, then model. The first word goes into the make slot, so a two-word make such as Land Rover puts "land" there. Check the first rows of a new query before you rely on it.
-
Narrow on the site, not afterwards.
condition, the year, price and mileage ranges,stateandtransmissiongo into the Webmotors search URL, so you read only pages you want. -
Missing values drop out under a filter. With
maxMileageKmset, a row without an odometer reading is skipped; with a year range, a row without a year is skipped. -
Keep each query to make and model. A version word joins the model:
"honda civic hybrid"becomes the pathhonda/civic-hybrid, which Webmotors does not recognise, so the Actor skips that query with a warning instead of pushing every Honda. -
Pick a fixed sort for repeat runs.
lowest_price,highest_price,lowest_kmandnewest_yearmap to the site's own sort orders, so two datasets are sorted the same way. -
Start small. Begin with
maxItems: 20to check the fields on one page of results, then raise it.
Troubleshooting
-
Run FAILED with "0 results for queries ...": a blocked run or one that cannot read a page ends
FAILED; a search that loads normally but matches nothing endsSUCCEEDEDwith an empty dataset and a status message saying so. Loosen your filters or check the make and model spelling. -
Run not started: a value outside a fixed list (
condition,transmission,sortBy) is refused by the platform before the run starts. -
A query that stops early: when a page keeps failing, the Actor ends that query, logs
Aborting querywith the error, and moves on to the next one. -
Fewer rows than expected: the run's summary line reads
Webmotors run finished: pushed N rows. Rejections summary: {...}, a count of dropped rows per reason (a filter, a duplicate id, or a listing it could not parse). Read it from Python withclient.log(run.id).get(). For help, use the Store page.
Pricing and next steps
For current pricing and details, see the Store page.
To repeat a search, save it as a task and add an Apify schedule, which runs Actors and tasks on a cron expression. A webhook on ACTOR.RUN.SUCCEEDED sends the run object, including defaultDatasetId, to your endpoint.
Links:
Which makes or states are you planning to track on Webmotors? Drop a comment! 💬
Top comments (0)