DEV Community

Cover image for How to Scrape Zap Imóveis Property Listings with Python
ReventaData
ReventaData

Posted on Edited on Originally published at apify.com

How to Scrape Zap Imóveis Property Listings with Python

By the end of this tutorial, you will have a short Python script that turns a Zap Imóveis search into structured rows: asking price in reais, monthly condominium fee, IPTU, bedrooms, bathrooms, area in m², street, neighbourhood and amenities.

Zap Imóveis (zapimoveis.com.br) is a Brazilian property portal. It sits behind a Cloudflare managed challenge: in testing on 2026-10-17, direct, datacenter and residential requests got HTTP 403, while Apify Unblocker returned the full page.

In this guide, we use Python and the Apify Actor Zap Imóveis Scraper (reventadata/zapimoveis-scraper). You will pick a city, a deal type and a property type, run the Actor and read the rows. The sample row below is a sample from a capture dated 2026-10-24, parsed with the Actor's own parser.

⏱️ Time estimate: one sitting: install one package, set one token, run three short Python blocks.


Prerequisites

To follow this tutorial, you will need:

  1. Python 3.11+ (apify-client 3.3.0 requires it).
  2. The Apify API client for Python, version 3.
  3. An Apify account and its API token, which sits on the API & Integrations page in Apify Console.

Install the client:

pip install -U "apify-client~=3.0"
Enter fullscreen mode Exit fullscreen mode

The code reads the token from an environment variable:

export APIFY_API_TOKEN="your_apify_token_here"
Enter fullscreen mode Exit fullscreen mode

What you'll achieve

You will turn one Zap Imóveis search into one row per listing. Each row carries the listing id, url and title, price and currency (BRL), condoFee and iptu, bedrooms, bathrooms, parkingSpaces, areaM2, the address (street, neighborhood, city, state), advertiser, amenities and photo links in images (five on most captured rows). propertyType is read from the title, for example apartamento.

Example use case: Suppose you want two-bedroom apartments for sale in São Paulo under R$ 600,000. You would set location: "sp+sao-paulo", operation: "venda", propertyType: "apartamentos", minBedrooms: 2 and maxPrice: 600000. The Actor adds those bounds to the search URL and checks every row against them again before saving it.


Step 1: Access Zap Imóveis Scraper

Go to the Zap Imóveis 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

Three inputs are required and together they pick the Zap Imóveis listing page:

  • location: a {state}+{city} slug such as sp+sao-paulo, rj+rio-de-janeiro, pr+curitiba or mg+belo-horizonte. The Actor lowercases it and turns sp-sao-paulo or sp sao paulo into sp+sao-paulo.
  • operation: venda (for sale) or aluguel (for rent).
  • propertyType: apartamentos, casas, imoveis (all), comerciais or terrenos.

maxItems (default 100) caps the rows saved. Here is the setup. It asks for apartments for sale in 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: apartments for sale in São Paulo, at most 10 rows
run_input = {
    "location": "sp+sao-paulo",
    "operation": "venda",
    "propertyType": "apartamentos",
    "maxItems": 10,
}
Enter fullscreen mode Exit fullscreen mode

💡 Tip: operation and propertyType only take the values listed above. Anything else is refused before the run starts, so a typo never starts a run.

No proxy setting is needed in run_input: when none is given, the Actor uses Apify Unblocker, the transport that returned real pages in testing on 2026-10-17.


Step 3: Add optional filters

Parameter Type What it does
minPrice / maxPrice integer Price band in reais. Sent to the site as precoMinimo / precoMaximo and checked on each row; a row with no price passes.
minBedrooms integer Sent as quartos and checked on each row; a row with no bedroom count is dropped.
minArea integer Sent as areaMinima and checked on each row; a row with no area is dropped.
searchQueries array Not a site search: a filter on the rows. A row is kept when every word of at least one entry appears in its title or description, ignoring case and accents.

Three combinations that work:

  • Rentals in Rio: location: "rj+rio-de-janeiro", operation: "aluguel", propertyType: "apartamentos".
  • Family houses in Curitiba: location: "pr+curitiba", propertyType: "casas", minBedrooms: 3, minArea: 120.
  • Apartments with a pool: searchQueries: ["piscina"]. It reads the title and description only: on the captured page, piscina was in no title and in 3 descriptions, while Pool was in the amenities of 18 rows.

Step 4: Run the Actor

Now start the run with client.actor("reventadata/zapimoveis-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 Zap Imóveis extraction...")
run = client.actor("reventadata/zapimoveis-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")
Enter fullscreen mode Exit fullscreen mode

Behind the scenes:

  • The Actor builds the search URL (/venda/apartamentos/sp+sao-paulo/ here, plus ?pagina=N from page 2 on) and fetches it through Apify Unblocker.
  • It reads the page's structured listing data, adds the condo fee, IPTU and parking spaces shown on each listing card, applies your filters, and saves the rows. A listing seen on an earlier page is not saved twice.
  • It stops at maxItems or after two empty pages in a row.

Step 5: View and download results

Loop over the rows to print the parts you need:

for item in items:
    print(item["title"])
    price = item.get("price")
    print(f"  Price: R$ {price:,.0f}" if price is not None else "  Price: not shown")
    if item.get("condoFee") is not None:
        print(f"  Condo fee: R$ {item['condoFee']:,.0f} per month")
    print(f"  {item.get('bedrooms')} bedrooms, {item.get('areaM2')} m²")
    print(f"  Where: {item.get('neighborhood') or '?'}, {item.get('city')} ({item.get('state')})")
    print("-" * 40)
Enter fullscreen mode Exit fullscreen mode

Here is one row, a sample from a capture dated 2026-10-24 (page 1 of apartments for sale in São Paulo) parsed with the Actor's own parser. A saved row also carries scrapedAt, the time the page was parsed:

{
    "id": "2865131669",
    "title": "Apartamento para comprar com 78 m², 2 quartos, 1 banheiro, 1 vaga em  Vila Paiva, São Paulo",
    "url": "https://www.zapimoveis.com.br/imovel/venda-apartamento-2-quartos-vila-paiva-zona-norte-sao-paulo-sp-78m2-id-2865131669/",
    "operation": "venda",
    "propertyType": "apartamento",
    "price": 535000.0,
    "currency": "BRL",
    "condoFee": 1200.0,
    "iptu": 129.0,
    "bedrooms": 2,
    "bathrooms": 1,
    "parkingSpaces": 1,
    "areaM2": 78.0,
    "street": "Rua Santo Anselmo",
    "neighborhood": "Vila Paiva",
    "city": "São Paulo",
    "state": "SP",
    "advertiser": "Vmx - Administração Profissional De Imóveis Ltda",
    "amenities": [
        "Electronic Gate",
        "Bathroom Cabinets",
        "Pets Allowed",
        "Service Area",
        "Elevator",
        "Builtin Wardrobe",
        "Lavabo",
        "Blindex Box",
        "Kitchen Cabinets",
        "Gated Community",
        "Planned Furniture",
        "Balcony",
        "Bedroom Wardrobe"
    ],
    "images": [
        "https://resizedimgs.zapimoveis.com.br/img/vr-listing/f872a3cbf962cad17599d6ce754a7458/apartamento-com-2-quartos-a-venda-78m-no-vila-paiva-sao-paulo.webp?action=fit-in&dimension=614x297",
        "https://resizedimgs.zapimoveis.com.br/img/vr-listing/cde256c0ba81ddef1a7919f88f185fd2/apartamento-com-2-quartos-a-venda-78m-no-vila-paiva-sao-paulo.webp?action=fit-in&dimension=614x297&seo=false",
        "https://resizedimgs.zapimoveis.com.br/img/vr-listing/5b3f304d7a809815511e553d49662984/apartamento-com-2-quartos-a-venda-78m-no-vila-paiva-sao-paulo.webp?action=fit-in&dimension=614x297&seo=false",
        "https://resizedimgs.zapimoveis.com.br/img/vr-listing/2be919917f874b23a74a9fde73ba143a/apartamento-com-2-quartos-a-venda-78m-no-vila-paiva-sao-paulo.webp?action=fit-in&dimension=614x297&seo=false",
        "https://resizedimgs.zapimoveis.com.br/img/vr-listing/5945376ad9289b5c91fb55a5d79a2e66/apartamento-com-2-quartos-a-venda-78m-no-vila-paiva-sao-paulo.webp?action=fit-in&dimension=614x297&seo=false"
    ],
    "description": null
}
Enter fullscreen mode Exit fullscreen mode

Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.


Understanding your results

The captured page gave 30 rows, all apartments in São Paulo, priced from R$ 230,000 to R$ 3,100,000. Read the fields like this:

  • price, currency: the asking price in reais (BRL). All 30 rows had one.
  • condoFee, iptu: monthly condominium fee and IPTU, the municipal property tax, as the listing publishes them. 23 rows had a condo fee and 9 an IPTU value.
  • bedrooms, bathrooms, areaM2: filled on all 30. 17 rows had two bedrooms, 8 three, 4 one and 1 four. When the title shows an area range, as on a new development (32 - 79 m²), areaM2 holds one figure from the page data, 32 in that case. 10 of the 30 titles showed a range.
  • parkingSpaces: read from the listing card; filled on 13 rows, though 23 titles mention a vaga.
  • neighborhood: taken from the title's em <bairro>, <city> ending; null on 7 rows.
  • advertiser: an agency name when the card shows one; filled on 5 rows.
  • amenities: English names from the page data. Pets Allowed appeared on 19 rows, Gym and Pool on 18 each.

Pro tips

  1. Sanity-check the fees. Values come as the listing publishes them. In the capture, one row's condo fee and IPTU were both 1.0, and another's condo fee was 980000.0, as the page's own data stated. Drop outliers before you average a column.
  2. Read the title when a field is null. parkingSpaces and neighborhood are often empty while the title still says 1 vaga or names the bairro.
  3. Use the price and size inputs, not just keywords. minPrice, maxPrice, minBedrooms and minArea narrow the URL the site serves; searchQueries only drops rows after the page is read.
  4. Watch for repeat listings. Two rows in the capture had different ids but the same street, price, area and condo fee. Deduplicating by id keeps both, so group on those fields if you count homes.
  5. Start small. Keep maxItems: 10 until your downstream code handles the fields, then raise it.

Troubleshooting

  • Run FAILED with zero rows: a blocked page, an unreadable page or an empty first result page ends the run FAILED; an over-strict filter on a page that loaded with listings ends SUCCEEDED with an empty dataset and a status message saying so. Loosen the filters and check the location slug.
  • Run stops early: a page that keeps returning a challenge is tried up to three times; after that the run stops reading pages and keeps what it saved.
  • Input refused before the run starts: operation or propertyType is not one of the listed values, or a required field is missing.
  • Fewer rows than expected: read the summary line, Scrape run completed: pushed N listing(s) (rejected during filtering: {...}); it counts drops per filter (keyword_mismatch, price_bounds, bedrooms_below_min, area_below_min). 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 Brazilian city's listings are you planning to track? Drop a comment! 💬

Top comments (0)