VivaReal Scraper: Brazil Property Listing Alerts
Pricing
from $1.52 / 1,000 listing returneds
VivaReal Scraper: Brazil Property Listing Alerts
VivaReal scraper by search: get one row per listing (price, condo fee, IPTU, area, bedrooms, bathrooms, parking, neighbourhood, link). Turn on monitoring for only new listings and price drops since your last run. No login required; empty searches and quiet monitoring runs are free.
Pricing
from $1.52 / 1,000 listing returneds
Rating
0.0
(0)
Developer
Adrian Voss
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Watch a property search, get only what's new. This actor runs your search against VivaReal's own search-results pages — one of Brazil's two largest real-estate marketplaces — and returns one clean row per listing: price, condo fee, IPTU, area, bedrooms, bathrooms, parking, neighbourhood, and a permanent link. Turn on monitoring and it becomes a watchlist: new listings and price drops since your last run, charged per new row, free on quiet days. Schedule it hourly and send the results to Discord, Slack, Google Sheets, or n8n.
Who it's for
A renter or buyer watching one city for newly-posted apartments or houses wants a live feed of what's actually on VivaReal right now, instead of refreshing the search page by hand — VivaReal has no saved-search alert of its own. An investor tracking a market for underpriced or recently discounted listings, a relocation agency monitoring several cities for clients at once, or a Sheets/n8n workflow that reacts the moment something new is posted, gets the same shape here: paste a location or a search URL, get back a flat row per listing (including condo fee and IPTU, which most scrapers skip), and pay only for listings actually returned — a search that finds nothing costs nothing.
VivaReal and ZAP Imoveis share inventory and a codebase. The same property is often listed on
both sites (Grupo OLX Brasil owns both). This actor's sibling, zapimoveis-listing-lookup, runs the
identical search and monitoring shape against ZAP Imoveis — dedupe across the two by listingId,
which is the same numeric ZAP/VivaReal property id on both. OLX Brasil's general classifieds site is
a different front end entirely and is covered by a separate actor, not this family.
Why this one
- Monitoring first, not a one-off dump. Turn on "Only return results that are new, or dropped in price" and this actor becomes a watchlist: it remembers every listing ID it has already shown you, and only bills you for what's actually new or has dropped in price since the last run. A quiet run — nothing new, nothing cheaper — still finishes and costs only the base run fee.
- Reads the condo fee and property tax, not just the headline price. Every card on VivaReal's
own search pages carries "Cond. R$ X • IPTU R$ Y" next to the price — this actor parses both into
their own
condoFeeandiptucolumns (0 when the listing marks them "isento"/exempt, empty when the site doesn't show the line at all), which most listing scrapers skip entirely. - A real posted date, not a guess. VivaReal's page ships a second, easy-to-miss JSON-LD block
(
RealEstateListing.mainEntity) that carries each listing's actualdatePosted— this actor reads it directly rather than fabricating one from "time ago" text. - Never charged for a miss, or a quiet monitoring run. A search that finds nothing, or a scheduled watchlist run where nothing changed, still gets a row explaining what happened — and costs nothing.
- Paste a location or your own saved search URL. Type VivaReal's own
<state-slug>/<city-slug>(e.g. "sp/sao-paulo", "minas-gerais/belo-horizonte"), or paste a VivaReal search-results URL you've already filtered on the site — its filters carry straight through.
What you get
One row per listing by default (turn off "Expand rows" in Advanced to get one row per search
instead, with the listings nested under listings).
| Field | Type | Description |
|---|---|---|
query | text | The search you submitted, echoed back |
found | boolean | true if VivaReal returned a real search-results page for this search |
status | text | OK on a match; NOT_FOUND, BLOCKED, or BAD_FORMAT on a miss |
message | text | Plain-English reason for a miss — only present when found is false |
searchQuery | text | The search line you submitted (location, or the pasted URL) |
listingCount | number | Listings returned and billed for this search |
truncated | boolean | true if a page couldn't be fetched (even after one retry) before the requested count was reached |
newCount | number | New listings this run (monitoring only) |
priceDropCount | number | Price drops this run (monitoring only) |
monitorStatus | text | NO_NEW_ROWS on a quiet monitoring run, WATCHLIST_SEEDED on a silent first run, otherwise empty |
listingId | text | VivaReal's own numeric listing ID (shared with ZAP Imoveis for the same property) |
title | text | Listing title |
operation | text | buy or rent |
price | number | Price in BRL |
currency | text | Always BRL |
condoFee | number | Monthly condo fee in BRL; 0 when the listing marks it "isento" (exempt) |
iptu | number | Annual property tax (IPTU) in BRL; 0 when marked "isento" |
areaM2 | number | Floor area in square metres |
bedrooms | number | Bedroom count |
bathrooms | number | Bathroom count |
parkingSpaces | number | Parking spaces |
floorLevel | text | Floor level, when VivaReal's listing data has one |
street | text | Street address |
neighbourhood | text | Neighbourhood (bairro) |
city | text | City |
state | text | State (two-letter UF code) |
adTier | text | VivaReal's own ad tier for this listing (e.g. "SUPER PREMIUM"), when it has one |
advertiserName | text | The advertiser's name, only when it is clearly a business/agency — a private seller's name is never output (see "Data & privacy") |
postedAt | date | The listing's real posted date, from VivaReal's own data |
photoCount | number | Number of photos VivaReal's listing data lists |
imageUrl | text (link) | First listing image |
description | text | Listing description, with any email/phone/WhatsApp-link-like text redacted |
changeType | text | new, price-drop, or seen (monitoring only) |
isNew | boolean | true on this listing's first appearance (monitoring only) |
previousPrice | number | The price last recorded for this listing, when it dropped (monitoring only) |
priceDropPct | number | Percentage drop from the previous recorded price (monitoring only) |
firstSeenAt | date | When this watchlist first saw this listing (monitoring only) |
url | text (link) | Permanent listing link |
scrapedAt | date | When this row was fetched |
Deselect any field in the Input tab's column picker to drop it from every row — query, found,
status, message, and scrapedAt always stay. This actor does not declare a sellerHash or
totalAvailable column: VivaReal's search pages don't expose an advertiser ID (so a hash can't be
computed) or a reliable total-match count on this route — see the FAQ.
Pricing
Pay-per-event. A flat per-run fee covers session/proxy warmup; you're billed
per item only when data is actually found and returned — see
.actor/pay_per_event.json for exact prices. A miss is never charged.
Monitoring / only new results
Turn on "Only return results that are new, or dropped in price" and a search line becomes a watchlist instead of a one-off dump:
- A listing counts as new the first time its VivaReal listing ID is returned by this watchlist.
- A listing counts as a price drop when its price falls by at least "Minimum price drop" (5% by default) since it was last seen — measured against the most recently seen price, so a listing that drops twice is compared against its latest price each time, not its original one.
- Already-seen, unchanged listings are dropped before you're billed. A quiet run — nothing new,
nothing cheaper — still finishes and returns one free row with
monitorStatus: NO_NEW_ROWS, so a scheduled task shows it actually ran. - The first run has nothing to compare against. By default it returns everything it finds as "new" (so you see your whole current search result once). Turn on "Seed silently" to instead have the first run bank everything quietly with zero rows and zero charge, and start alerting from the second run on — this is what stops a fresh watchlist from firing a hundred "new listing" messages into your Discord channel on day one.
- Results are sorted by each listing's own posted date whenever monitoring is on (VivaReal's own search pages carry no sort parameter to force this server-side), so two runs of the same watchlist are always comparing the same ordering.
Schedule recipe. Save this actor as an Apify Task with deltaMode: true and a
deltaName you choose, add an hourly (or daily) Schedule, and point the task's webhook at
Discord, Slack, Google Sheets, or an n8n/Make webhook URL to get pinged only when something
actually changes.
Price-drop window limitation. A price drop is only ever seen while the listing is still inside
what a run actually fetches — if a listing falls out of your maxListingsPerSearch window or your
price filters between runs, this actor has no way to notice it dropped. Keep searches reasonably
narrow (one city, a price band) rather than one huge catch-all if price-drop coverage matters to
you.
How to use
- In the Apify Console. Open the actor page and click Start — the
searchesfield is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found. - Via the API. Call it directly with a POST request — no Console needed once you have an API token:
curl "https://api.apify.com/v2/acts/accountable_eel~vivareal-listing-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \-X POST \-H "Content-Type: application/json" \-d '{"searches":["sp/sao-paulo"]}'
- On a schedule. Save this actor as an Apify Task with the input you want, then add a Schedule (hourly, daily, weekly) so it runs on its own — no server of your own required.
- Turn on "Only return results that are new, or dropped in price" under Monitoring once you want a watchlist instead of a one-off dump — see "Monitoring / only new results" above.
Input
{"searches": ["sp/sao-paulo"]}
One per line: paste a full vivareal.com.br search-results URL (its own filters are kept), or type a location as
operation (buy/rent) and propertyType (apartment, house, house in a condominium, land, or
commercial) apply to any search line that's a location rather than a pasted URL — a pasted URL
keeps whatever operation and property type it already has. VivaReal's own state slugs aren't all
2-letter codes (sp and rj are, but most others are spelled out in full, e.g. minas-gerais,
parana) — find the right one by browsing to that state on vivareal.com.br and reading it off the
URL, or just paste the URL directly.
Sample output
| query | found | status | searchQuery | listingCount | totalAvailable | truncated | newCount | priceDropCount | monitorStatus | listings | listingId | title | operation | price | currency | condoFee | iptu | areaM2 | bedrooms | bathrooms | parkingSpaces | floorLevel | street | neighbourhood | city | state | adTier | advertiserName | postedAt | photoCount | imageUrl | description | changeType | isNew | previousPrice | priceDropPct | firstSeenAt | url | scrapedAt |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| sp/sao-paulo | true | OK | <monitoring status (quiet / seeded runs)> | <all listings found (full list)> | <vivareal listing id (shared with zap imoveis for the same property)> | <buy or rent> | <price (brl)> | <condo fee (brl/month)> | <iptu property tax (brl/year)> | <area (m²)> | <state (uf)> | <ad tier (standard / premium / super premium)> | <advertiser (agencies only)> | <new / price-drop / seen> | <is this listing new?> | <previous price (brl)> | <price drop %> | 1970-01-01T00:00:00.000Z |
A search that finds nothing comes back as a row with "found": false and is never charged. A quiet
monitoring run comes back as a row with "found": true, "monitorStatus": "NO_NEW_ROWS" and is also
never charged.
Use it from Clay, n8n, Make, or an AI agent
This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.
curl "https://api.apify.com/v2/acts/accountable_eel~vivareal-listing-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \-X POST \-H "Content-Type: application/json" \-d '{"searches":["sp/sao-paulo"]}'
n8n. Add an HTTP Request node: Method POST, URL https://api.apify.com/v2/acts/accountable_eel~vivareal-listing-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>, Body Content Type JSON, JSON Body {"searches":["sp/sao-paulo"]} (swap in an expression from an earlier node for a real value).
Clay. Add an "HTTP API" column: Method POST, URL https://api.apify.com/v2/acts/accountable_eel~vivareal-listing-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>, Body {"searches":["{{search}}"]}, mapping the row's search into the searches array.
MCP. In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "VivaReal Scraper | Apify" — the agent will find and run this actor.
For monitoring, point the Task's webhook (Console → your Task → Webhooks) at a Discord or Slack incoming-webhook URL, a Google Sheets automation (via Zapier/Make/n8n listening for the webhook), or an n8n/Make workflow's own webhook trigger, so new listings and price drops land wherever you actually work instead of a dataset you have to remember to check.
Tips
- Start monitoring with "Seed silently" on for any watchlist you're setting up on a schedule — it avoids a wall of "new" alerts for your entire current search result on day one.
- Keep a monitored search reasonably narrow (one city, a price band) — see "Price-drop window limitation" above for why a huge catch-all search misses price drops on listings that fall outside what gets fetched.
- Dedupe against
zapimoveis-listing-lookuponlistingId— ZAP and VivaReal share inventory, so the same apartment can legitimately appear on both. - A
BLOCKEDstatus means VivaReal returned a 403/429/503, or an UNBLOCKER 502, even after this actor's own one retry on a fresh session — trying again shortly usually clears it. - Paste your own already-filtered VivaReal search URL instead of rebuilding filters in this actor's Input tab — anything the URL already filters on carries straight through.
vs. no dedicated leader
S3b (2026-09-18 research) found no strong dedicated VivaReal actor on the Apify Store — unlike ZAP Imoveis, which has an established leader. This actor adds new-listing and price-drop monitoring as a first-class mode, reads the condo fee and IPTU that most scrapers skip, and bills only for listings actually delivered.
| What it costs | What you get | Trade-off | |
|---|---|---|---|
| This actor | Pay per listing returned; nothing for a miss or a quiet monitoring run | Condo fee, IPTU, real posted date, monitoring mode (new + price drop) | An advertiser's name is output only when it's clearly an agency; no seller ID exists on this route to hash or dedupe by |
| Doing it yourself | Your time to build and maintain a scraper, a seen-listing store, and a price-tracking layer | The same data | This actor's dual JSON-LD parsing, monitoring state, and redaction are the maintenance burden it absorbs |
No dedicated actor covers cross-marketplace monitoring: the GitHub project imovel-bot scores
listings 0-100 across OLX, ZAP and QuintoAndar by sweeping them itself — this actor and its sibling
zapimoveis-listing-lookup are the hosted, schedulable version of that job for Grupo OLX Brasil's
two property sites.
Data & privacy
This actor reads public search results that anyone can see without logging in. It doesn't log in,
solve CAPTCHAs, or reveal hidden contact details. Seller identity is off by default: VivaReal
doesn't expose an advertiser ID anywhere in the pages this actor reads, so this actor only ever
outputs an advertiser's display name when it clearly reads as a business or agency — never a
private seller's name. Agent and owner phone numbers, WhatsApp numbers and emails are never
collected, and any that appear inside a listing's description are redacted to [redacted]. Not
affiliated with VivaReal / Grupo OLX Brasil.
LGPD. Brazil's Lei Geral de Proteção de Dados is GDPR-like. Seller info is off by default here, and — unlike a toggle a buyer could turn on — a private seller's name is never output by this actor at all, on this route.
Is this allowed? It collects the same public listing data your browser shows, for the searches you choose. It's built for monitoring a search, not for copying the marketplace. Check that your use fits VivaReal's terms and your local law.
FAQ
Why is a row empty, or why does found say false?
Either the input line was blank (status: BAD_FORMAT), the location was missing a state or city
slug (status: BAD_FORMAT), VivaReal returned a 403/429/503 or an UNBLOCKER 502 even after one
retry (status: BLOCKED), or the search matched nothing on VivaReal right now (
status: NOT_FOUNDmessage column for the specific reason. None of these are billed.
Am I charged for a quiet monitoring run?
No. When monitoring finds nothing new and no price drops, you get one row with
monitorStatus: NO_NEW_ROWS and listingCount: 0 — Actor.charge() only fires when
listingCount is greater than zero.
Why isn't there a sellerHash or seller ID column?
VivaReal's search-results pages don't expose an advertiser ID anywhere this actor can read — only
a display name, sometimes a private person's, sometimes an agency's. Rather than fabricate an ID or
hash a name (which would leak a private seller's identity through the hash's stability), this actor
outputs advertiserName only when the name is clearly a business, and nothing at all otherwise.
Why isn't there a totalAvailable column?
VivaReal's search pages don't carry a reliable "N properties match this search" total on the static
route this actor reads (confirmed directly, not assumed) — so rather than show a stale or partial
number, this actor doesn't declare the column at all.
What does truncated: true mean?
This actor tried to fetch another page toward your maxListingsPerSearch limit and that page came
back blocked (or with an empty/unreadable body) even after one retry on a fresh UNBLOCKER session —
so you got fewer listings than requested. Try again shortly.
How do I find the right state slug for my city?
Browse to that state and city on vivareal.com.br and read the two path segments after /venda/ or
/aluguel/ — sp and rj are 2-letter codes, most others are the state's full name
(minas-gerais, parana, rio-grande-do-sul, espirito-santo, confirmed for those four). Or just
paste the full search URL as a search line instead — it always works.
Do I need to configure proxies? This actor requires Apify's UNBLOCKER proxy group by default — VivaReal is Cloudflare-protected and blocks plain datacenter and residential requests alike. UNBLOCKER is already configured; you don't need to change anything.
Can an AI agent call this directly? Yes. It's registered on the Apify MCP server — an agent in Claude, Cursor, or another MCP client can find and run it by name ("VivaReal Scraper | Apify"), or you can call the REST endpoint shown above from any script or workflow tool.
Related actors
- ZAP Imoveis Scraper — the same
search and monitoring shape for ZAP Imoveis, which shares inventory and a codebase with VivaReal
(both owned by Grupo OLX Brasil). Dedupe the two on
listingId.