By the end of this tutorial, you will have a short Python script that turns a QuintoAndar search into structured rows: property type, monthly rent or sale price, the site's total monthly cost, condominium and IPTU fees, floor area, bedrooms, bathrooms, parking spots and street address.
QuintoAndar is a Brazilian site for renting and buying homes; its search pages embed the listing data as JSON. When the site was measured on 2026-10-17, direct requests got those pages without a proxy, so the Actor runs without one by default.
In this guide, we use Python and the Apify Actor QuintoAndar Brazil Scraper (reventadata/quintoandar-scraper). You will configure a search, run it, and filter the rows. The sample row below is a sample from a capture dated 2026-10-17, parsed with the Actor's own row builder.
⏱️ 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+ (
apify-client3.3.0 requires it). - The Apify API client for Python.
- An Apify account and its API token, which sits on the API & Integrations page in Apify Console.
Install the client:
pip install "apify-client~=3.0"
The code reads the token from an environment variable:
export APIFY_API_TOKEN="your_apify_token_here"
What you'll achieve
You will turn a QuintoAndar search into one row per listing. Each row carries the listing id and url, businessType (RENT or SALE), propertyType (Apartamento, Casa and so on), rentPrice, salePrice, totalCost and condoIptu in BRL, area in square metres, bedrooms, bathrooms, parkingSpots, isFurnished, street, neighbourhood, city, amenities and photo fields.
Note: Rows hold listing data only. No row field carries an owner's name, CPF or phone number.
Example use case: Suppose you want two-bedroom rentals in Moema of at least 60 m² under 6,000 BRL a month. You would set searchQueries: ["moema"], businessType: "RENT", minBedrooms: 2, minArea: 60 and maxPrice: 6000, and every saved row would meet all three bounds.
Step 1: Access QuintoAndar Brazil Scraper
Go to the QuintoAndar Brazil Scraper page on Apify Store. The README lists every input and output field. In this tutorial we drive the Actor from Python, so everything after this step happens in code.
Step 2: Configure the search
The main input is searchQueries: one QuintoAndar search per entry. Each entry is one of two things:
-
A QuintoAndar URL, such as
https://www.quintoandar.com.br/alugar/imovel/moema-sao-paulo-sp-brasil. The Actor uses it as is. -
A place name. The Actor knows 29 neighbourhoods and cities, among them
pinheiros,moema,itaim bibi,copacabana,ipanema,savassi,batel,curitiba,porto alegreandrecife. Case and accents do not matter, and other words in the entry are ignored once a known name is found.
businessType picks rentals (RENT, the default) or sales (SALE). maxItems (default 50) caps the rows saved across all searches.
Here is the setup. It searches rentals in Pinheiros, São Paulo, and saves at most 10 rows:
import os
from apify_client import ApifyClient
# 1. Initialize ApifyClient with the token from the environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
# 2. Define the search: rentals in Pinheiros, Sao Paulo, at most 10 rows
run_input = {
"searchQueries": ["apartamento pinheiros sao paulo"],
"businessType": "RENT",
"maxItems": 10,
}
💡 Tip: the word apartamento in that query does not filter anything; the Actor reads it as the place pinheiros. To keep apartments only, set propertyType: "apartamento".
⚠️ For a place outside the 29 names, paste the URL from QuintoAndar's own search. Other text becomes a guessed address:
casa jardim botanico curitibaresolves to all of Curitiba, andVila Madalenatovila-madalena-brasil, a path that was not checked against the site.
Step 3: Add optional filters
These inputs narrow what gets saved. Rows a filter drops never reach your dataset:
| Parameter | Type | What it keeps |
|---|---|---|
propertyType |
string |
all, apartamento, casa, kitnet or studio. Adds the type to the search address and keeps rows whose type contains it. |
minPrice / maxPrice
|
integer | Rent for RENT, sale price for SALE, in BRL. Rows without that price are dropped when either is set. |
minBedrooms |
integer | At least N bedrooms. Rows without a count are dropped. |
minArea |
integer | At least N m². Rows without an area are dropped. |
Three example combinations:
-
Family rentals in Copacabana:
searchQueries: ["copacabana"],minBedrooms: 3. -
Apartments for sale in Moema:
searchQueries: ["moema"],businessType: "SALE",propertyType: "apartamento". -
Large houses in Curitiba:
searchQueries: ["curitiba"],propertyType: "casa",minArea: 150.
⚠️
businessTypeandpropertyTypeaccept only their listed values; anything else is refused before the run starts.
Step 4: Run the Actor
Now start the run with client.actor("reventadata/quintoandar-scraper").call(run_input=run_input). The call starts the Actor on Apify and waits for it to finish before returning the run:
# 3. Start the Actor run and wait for it to finish
print("Starting QuintoAndar extraction...")
run = client.actor("reventadata/quintoandar-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 saved listings
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} listings.\n")
Behind the scenes:
- The Actor turns each entry into a search address (
/alugar/imovel/pinheiros-sao-paulo-sp-brasilhere;/comprar/for sales). - It fetches that one page and reads the listings from the JSON the page embeds. It does not follow further result pages.
- It applies your filters, skips a listing already saved under an earlier entry, and stops at
maxItems.
Step 5: View and download results
Loop over the rows to print the parts you need:
for item in items:
print(item["title"])
print(f" Rent: {item.get('rentPrice')} BRL, total: {item.get('totalCost')} BRL")
print(f" Area: {item.get('area')} m2, bedrooms: {item.get('bedrooms')}")
print(f" Address: {item.get('street')}, {item.get('neighbourhood')}")
print(f" {item['url']}")
print("-" * 40)
Here is one row, a sample from a capture dated 2026-10-17, parsed with the Actor's own row builder. It is not the output of the input above. A saved row also carries scrapedAt, the time the row was built:
{
"id": "893228604",
"title": "Casa com 1 quarto(s) em Vila Nivi, São Paulo",
"url": "https://www.quintoandar.com.br/imovel/893228604",
"businessType": "RENT",
"propertyType": "Casa",
"rentPrice": 1400.0,
"salePrice": 0.0,
"totalCost": 1536.0,
"condoIptu": 76.0,
"area": 80,
"bedrooms": 1,
"bathrooms": 1,
"parkingSpots": 0,
"isFurnished": false,
"street": "Travessa Ortega",
"neighbourhood": "Vila Nivi",
"city": "São Paulo",
"state": null,
"amenities": [
"Energia",
"Varanda",
"Area De Servico"
],
"photoCount": 1,
"bannerPhoto": "https://www.quintoandar.com.br/img/893228604-891.203226451516MG0397.jpg",
"searchQuery": "vila nivi"
}
Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
rentPrice,salePrice: the site's prices in BRL. The sample rental showssalePrice: 0.0, not null, so treat 0 as "not for sale". -
totalCost: the site's monthly total. The dataset schema describes it as rent plus condominium, IPTU and insurance. In the sample it is 1,536 against a rent of 1,400. -
condoIptu: condominium fee and IPTU (municipal property tax) together, 76 in the sample. The remaining 60 of the gap is not split out in the row. -
title: built by the Actor from type, bedrooms, neighbourhood and city, not the advertiser's headline. -
amenities: the site's codes, made readable:AREA_DE_SERVICObecomesArea De Servico, with underscores turned into spaces. -
neighbourhood: the listing's neighbourhood, or its region when none is given. -
state: always null in this version; usecity. -
searchQuery: the input entry that produced the row, handy when one run covers several neighbourhoods.
Pro tips
-
Use
propertyType, not words in the query. Query text only picks the place.propertyTypechanges the search address and filters the rows. -
Paste URLs for places outside the list. Run a search on QuintoAndar in your browser, copy the address, and put it in
searchQueries. That is the only form the Actor never rewrites. -
Batch neighbourhoods in one run. The Actor reads one page per entry, so list several (
["pinheiros", "moema", "perdizes"]) to cover more ground. A listing kept under one entry is not saved again under the next. -
Compare like with like. For monthly budgets use
totalCost, notrentPrice; the gap includes condominium and IPTU. -
Read
isFurnishedcarefully. It is false when the listing does not say, so false means "not marked furnished" rather than "unfurnished". -
Start small. Keep
maxItems: 10until your downstream code handles the fields, then raise it step by step.
Troubleshooting
- Run FAILED with zero rows: a run that saves no rows ends FAILED, whether the results page returned no listings, could not be fetched, or every listing was dropped by your filters. Loosen the filters or check the place.
-
Rows from the wrong area: read the log line
Resolved query "..." to URL: ...for each entry. If the address is not the one you meant, paste the QuintoAndar URL instead. -
Fewer rows than expected: the log line
Query "...": found N listings, kept Kshows how many the page held and how many passed your filters. From Python:client.log(run.id).get().
For help, use the Actor's Store page.
Pricing and next steps
For current pricing and details, see the Store page.
To repeat a search, save the input as a task and add an Apify schedule, which runs an Actor or task on a cron expression (0 8 * * * is every day at 8 AM). To react when a run finishes, add a webhook on ACTOR.RUN.SUCCEEDED; its payload carries the run object, including defaultDatasetId.
Which neighbourhoods are you planning to track on QuintoAndar? Drop a comment! 💬
Top comments (0)