# VivaReal & ZAP Imóveis Scraper - Brazil Property Data (`dami_studio/vivareal-zap-imoveis-scraper`) Actor

Brazilian property listings from VivaReal and ZAP Imóveis in one run: asking price, condo fee, IPTU, usable area, bedrooms, bathrooms, parking, full address, coordinates, amenities, advertiser and photos. Sale or rental, by city or neighbourhood. Pay only for listings returned.

- **URL**: https://apify.com/dami\_studio/vivareal-zap-imoveis-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 property returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## VivaReal & ZAP Imóveis Scraper — Brazilian property listings

Point it at a Brazilian city or neighbourhood and it returns one row per property advert: asking
price, condomínio, IPTU, área útil, bedrooms, bathrooms, parking, the full address with coordinates,
the amenity list, who is advertising it, every photo and the link back to the listing. Sale and
rental both work.

Start with the limit, because it decides whether this is the right tool for you: **the portals stop
paging any single search at 1,500 results.** Ask for São Paulo and you will get 1,500 rows out of
the million-plus that exist. That is the portal's own window, not a setting here, and no scraper
gets past it. The way round it is to split the job by neighbourhood, by price band or by property
type, then run several searches. A Pinheiros three-bedroom search between R$800k and R$1.5m comes
back complete. "Every flat in Brazil" does not.

### What one row looks like

Real row, trimmed to the interesting parts, pulled on 19 September 2026:

```json
{
  "listingId": "2814207417",
  "portal": "VivaReal",
  "url": "https://www.vivareal.com.br/imovel/apartamento-2-quartos-pinheiros-zona-oeste-sao-paulo-com-garagem-85m2-venda-RS2650000-id-2814207417/",
  "title": "Apartamento com 2 Quartos à venda, 85m²",
  "businessType": "SALE",
  "listingType": "USED",
  "currency": "BRL",
  "price": 2650000,
  "condoFee": 2060,
  "iptu": 890,
  "iptuPeriod": "YEARLY",
  "usableArea": 85,
  "bedrooms": 2,
  "bathrooms": 1,
  "suites": 0,
  "parkingSpaces": 1,
  "unitFloor": 13,
  "amenities": ["POOL", "GYM", "ELEVATOR", "BARBECUE_GRILL", "GATED_COMMUNITY"],
  "street": "Rua Dona Ana Neri",
  "streetNumber": "581",
  "neighborhood": "Pinheiros",
  "zone": "Zona Oeste",
  "city": "São Paulo",
  "stateAcronym": "SP",
  "zipCode": "01522000",
  "latitude": -23.559503,
  "longitude": -46.615471,
  "advertiserType": "agency",
  "advertiserName": "Santucci Broker Negócios Imobiliários",
  "advertiserLicense": "31016-J-SP",
  "phones": ["11999782608"],
  "whatsapp": "11999782608",
  "imageCount": 30,
  "createdAt": "2024-05-29T20:07:12.572Z",
  "updatedAt": "2026-08-26T17:47:06.754Z"
}
```

Rentals carry two extra numbers that matter in Brazil: `price` is the monthly rent on its own and
`rentalTotalPrice` is what the tenant actually pays each month once condomínio and IPTU are added.
Measured on a Pinheiros rental pull: R$12,800 rent, R$14,860 total. Budget filters that ignore the
condomínio are why so many Brazilian rental searches lie to you.

### Input

Everything except the place is optional.

```json
{
  "location": "Pinheiros, São Paulo",
  "portal": "vivareal",
  "business": "SALE",
  "listingType": "USED",
  "bedroomsMin": 3,
  "priceMin": 800000,
  "priceMax": 1500000,
  "maxItems": 200
}
```

**location**. Write it the way a Brazilian would: `Pinheiros, São Paulo`, `Copacabana, Rio de
Janeiro`, `Curitiba`, `Balneário Camboriú`. A bare name is read as the city; add the city after a
comma when you mean a neighbourhood. That matters more than it sounds, because `São Paulo` is also
the name of a neighbourhood in Belo Horizonte. The run log names the place it picked on the first
line, so a wrong match shows up there rather than in the data. Fifteen of fifteen test phrasings,
accented and not, resolved to the place a person would have meant.

**portal**. VivaReal and ZAP Imóveis run on one shared listing index. Same properties, same ids,
same prices. What changes is which site the `url` points at and the order results come back in. Pick
whichever one your team already uses. Checked across eight identical searches on 19 September 2026:
the match counts agreed to the row every time.

**startUrls**. Paste search pages or single listing pages from either site instead of filling in
fields. A listing page has to end in `-id-<number>/`. A search page works best when it still has its
`?onde=` parameter, because that is where the portals keep the place id; copy the URL straight out
of the address bar after you have run the search on the site and it will. A plain SEO path like
`/venda/sp/sao-paulo/zona-oeste/pinheiros/` also works, since the last part of the path gets sent through
the place lookup, but that is a guess at your intent rather than a certainty.

**Filters**. `bedroomsMin`, `bathroomsMin`, `parkingMin`, `priceMin`, `priceMax`, `areaMin`,
`areaMax`, `unitTypes`. These are applied by the portal, not filtered out afterwards, so the count
in the log is the real count and you are not charged for rows that get thrown away. Verified: a
three-bedroom search between R$800k and R$1.5m returned ten rows, all with exactly three bedrooms,
all inside the band.

**locationIds**, for repeat runs. Once the log has told you that "Pinheiros, São Paulo" resolves to
`BR>Sao Paulo>NULL>Sao Paulo>Zona Oeste>Pinheiros`, put that string here and the place lookup is
skipped entirely. Same area every run, no ambiguity.

### What you get charged for

One charge per listing row delivered. Nothing else bills:

- An empty input writes a single labelled sample row and stops. Not charged.
- A place the portal does not recognise writes a diagnostic row saying so. Not charged.
- A URL that is not from either site writes a diagnostic row naming the URL. Not charged.
- A search that finds nothing writes a diagnostic row. Not charged.
- A refused request writes a diagnostic row after the retries run out. Not charged.

Diagnostic rows are marked `_diagnostic: true` and the sample row `_sample: true`, so a filter on
those two fields separates paid rows from free ones. The rate itself is on the pricing tab of this
page.

### What this does not do

- **It cannot go past 1,500 rows in one search.** Covered at the top; it is the single thing most
  likely to surprise you.
- **The count in the log is the portal's index count, not the headline on the website.** For
  Pinheiros the index reports 45,589 matches while the site's own page says 23,566. The rows are
  real either way, and every one of them opens on the live site, but the two numbers are counted
  differently and I could not reproduce the site's arithmetic. Treat the log count as a rough sense
  of scale and the rows as the truth.
- **Neighbourhood searches bleed slightly into the neighbours.** Over 270 Pinheiros rows, 261 were
  in Pinheiros and 9 were in Cerqueira César, Butantã and Itaim Bibi. That is the portal's own
  behaviour, not a bug here. Every row carries `neighborhood`, so filter on it if you need a hard
  boundary.
- **No sold prices and no price history.** The portals do not publish either. A row is what the
  advert says today.
- **No login-only data.** No saved searches, no lead forms, no messaging an advertiser, no account
  of any kind is created or used.
- **About one row in four has no coordinates, and it varies a lot by city.** Measured over 720 rows
  across eight cities: 77% carried a point, from 49% in Belo Horizonte up to 92% in Curitiba. Some
  advertisers hide the exact position. `latitude` and `longitude` come back null rather than
  guessed, so a map build should expect to drop rows rather than plot a wrong pin.
- **`advertiserType` is `agency`, `owner` or null.** It comes from the portal's own contract flag.
  Over 240 sampled rows it read agency on 232 and owner on 8. Private sellers are genuinely rare
  on these two sites, which is worth knowing before you build a for-sale-by-owner pipeline on it.
- **New developments (lançamentos) are a small pool.** Around 3,900 nationally against millions of
  resale adverts. Set `listingType` to DEVELOPMENT and expect hundreds, not thousands.
- **Descriptions are the advertiser's text.** HTML is stripped and line breaks kept; nothing is
  summarised, translated or cleaned up beyond that.

### Questions people actually ask

**Is this the VivaReal API or the ZAP Imóveis API?**
Both, and it is the same one. The two portals are run by the same company on one shared listing
index, which is why a listing id from one resolves on the other. The `portal` input decides which
site's URLs you get back.

**Can I scrape a whole city?**
Not in one go; see the 1,500 limit. Split it. For São Paulo, running one search per neighbourhood
gets you a lot further than one search for the city, and running each of those split by price band
gets you further again.

**Do I need a Brazilian IP address, a proxy or an account?**
No. Leave the proxy field alone; the Actor handles its own routing and rotates to another address
when the portal refuses one. If you have your own proxy servers and a reason to use them, put them
in `proxyConfiguration.proxyUrls` and they will be used as-is.

**How fresh is the data?**
Live at the moment of the run. Every row carries `updatedAt` from the advertiser's last edit, so you
can tell a listing refreshed last week from one that has sat untouched since 2024.

**Why did I get fewer rows than `maxItems`?**
Either the search genuinely has fewer matches (the log says how many the portal reports), or you
hit the 1,500 window, or duplicate ids were dropped. The same property is sometimes advertised by
several agencies; ids are deduplicated within a run, so you are charged once for it, not three
times.

**What currency are the prices in?**
Brazilian reais, always. `currency` is `"BRL"` on every row. `price`, `condoFee` and `iptu` are plain
numbers with no formatting, so they go straight into a spreadsheet or a database column.

**Can I get the advertiser's phone number?**
It is in `phones` and `whatsapp` when the advertiser published it, which is most of the time. It is
the number shown on the public listing page, nothing more.

**Does it work for commercial property and land?**
Yes. Set `unitTypes` to OFFICE, BUSINESS, COMMERCIAL\_PROPERTY, COMMERCIAL\_BUILDING, FARM or one of
the land types. `usageTypes` on each row tells you RESIDENTIAL or COMMERCIAL.

### Output fields

`listingId` · `portal` · `url` · `title` · `description` · `businessType` · `listingType` ·
`propertyType` · `unitTypes` · `usageTypes` · `currency` · `price` · `rentalTotalPrice` ·
`rentalPeriod` · `condoFee` · `iptu` · `iptuPeriod` · `usableArea` · `totalArea` · `bedrooms` ·
`bathrooms` · `suites` · `parkingSpaces` · `unitFloor` · `amenities` · `street` · `streetNumber` ·
`neighborhood` · `zone` · `city` · `state` · `stateAcronym` · `zipCode` · `fullAddress` ·
`latitude` · `longitude` · `advertiserType` · `advertiserName` · `advertiserId` ·
`advertiserLicense` · `advertiserTier` · `phones` · `whatsapp` · `publicationType` · `status` ·
`imageUrl` · `imageCount` · `images` · `createdAt` · `updatedAt` · `searchLocation` · `position` ·
`scrapedAt`

`amenities` is the portal's own code list: POOL, GYM, ELEVATOR, BARBECUE\_GRILL, GATED\_COMMUNITY and
about 160 others. They are passed through untranslated so they stay stable across runs.
`advertiserLicense` is the CRECI registration number where the advertiser is a licensed agency.

# Actor input Schema

## `location` (type: `string`):

A Brazilian city or neighbourhood, written the way it is written there: "Pinheiros, São Paulo", "Copacabana, Rio de Janeiro", "Curitiba", "Balneário Camboriú". The portal's own place lookup decides the match, and the run log tells you which one it picked.

## `portal` (type: `string`):

VivaReal and ZAP Imóveis run on one shared listing index, so both return the same properties. This only decides which site the listing URL points at, and the order results come back in.

## `business` (type: `string`):

Sale (venda) or rental (aluguel). Rental rows also carry the monthly total with condo fee and IPTU added.

## `listingType` (type: `string`):

USED is the ordinary resale market. DEVELOPMENT is new-build launches (lançamentos), a much smaller pool — a few thousand nationally rather than millions.

## `maxItems` (type: `integer`):

Hard cap on rows across every search in this run (1–5000). You are charged per listing returned. Sample and diagnostic rows are never charged.

## `startUrls` (type: `array`):

Paste search pages or single listing pages from either portal. A search page works best when it still carries its "?onde=" parameter — that is where the portal keeps the place id. A single listing page must end in -id-<number>/.

## `unitTypes` (type: `array`):

Leave empty for everything. These are the portal's own codes.

## `bedroomsMin` (type: `integer`):

Minimum number of bedrooms (quartos).

## `bathroomsMin` (type: `integer`):

Minimum number of bathrooms (banheiros).

## `parkingMin` (type: `integer`):

Minimum number of parking spaces (vagas).

## `priceMin` (type: `integer`):

Sale price, or monthly rent when you are searching rentals.

## `priceMax` (type: `integer`):

Sale price, or monthly rent when you are searching rentals.

## `areaMin` (type: `integer`):

Minimum área útil in square metres.

## `areaMax` (type: `integer`):

Maximum área útil in square metres.

## `locationIds` (type: `array`):

For repeat runs where you already know the exact place. These look like BR>Sao Paulo>NULL>Sao Paulo>Zona Oeste>Pinheiros and appear in the run log whenever a location is resolved. Using them skips the place lookup and guarantees the same area every time.

## `proxyConfiguration` (type: `object`):

Leave this alone. The Actor already routes itself and rotates addresses when the portal refuses one. Set it only if you must exit from a specific network of your own.

## Actor input object example

```json
{
  "location": "Pinheiros, São Paulo",
  "portal": "vivareal",
  "business": "SALE",
  "listingType": "USED",
  "maxItems": 100,
  "startUrls": [],
  "unitTypes": [],
  "locationIds": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Every row in the default dataset: listingId, portal, url, title, description, businessType, listingType, propertyType, unitTypes, usageTypes, currency, price, rentalTotalPrice, rentalPeriod, condoFee, iptu, iptuPeriod, usableArea, totalArea, bedrooms, bathrooms, suites, parkingSpaces, unitFloor, amenities, street, streetNumber, neighborhood, zone, city, state, stateAcronym, zipCode, fullAddress, latitude, longitude, advertiserType, advertiserName, advertiserId, advertiserLicense, advertiserTier, phones, whatsapp, publicationType, status, imageUrl, imageCount, images, createdAt, updatedAt, searchLocation, position and scrapedAt. An empty run, an unknown place or a refused request writes a single uncharged row explaining what happened instead.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "location": "Pinheiros, São Paulo",
    "portal": "vivareal",
    "business": "SALE",
    "listingType": "USED",
    "maxItems": 100,
    "startUrls": [],
    "unitTypes": [],
    "locationIds": [],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/vivareal-zap-imoveis-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "location": "Pinheiros, São Paulo",
    "portal": "vivareal",
    "business": "SALE",
    "listingType": "USED",
    "maxItems": 100,
    "startUrls": [],
    "unitTypes": [],
    "locationIds": [],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/vivareal-zap-imoveis-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "location": "Pinheiros, São Paulo",
  "portal": "vivareal",
  "business": "SALE",
  "listingType": "USED",
  "maxItems": 100,
  "startUrls": [],
  "unitTypes": [],
  "locationIds": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call dami_studio/vivareal-zap-imoveis-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/vivareal-zap-imoveis-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/tZVnx5YLePNDh3dmh/builds/ETKjEOOAcwTserplF/openapi.json
