Portugal Real Estate Scraper (Imovirtual, OLX, Idealista)
Pricing
from $5.00 / 1,000 listing with contact details
Portugal Real Estate Scraper (Imovirtual, OLX, Idealista)
One normalized dataset from Imovirtual, OLX, CustoJusto and Idealista: price, EUR/m2, T0-T5 typology, parish/municipality, private seller vs agency (native portal flags), phone, AMI licence, no-agencies flag, price history and days on market. Built for Portuguese agents, CRMs and market analysts.
Pricing
from $5.00 / 1,000 listing with contact details
Rating
0.0
(0)
Developer
Shalon Soluções Tecnologicas
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Portugal Real Estate Scraper — Imovirtual, OLX, CustoJusto, Idealista (one schema)
Scrape Portuguese property portals into one normalized dataset: price, €/m², typology (T0–T5), parish / municipality / district, private seller vs. agency, phone, AMI licence, "no agencies" flag, price history and days on market. Built by a Portuguese proptech founder for real-estate agents, CRMs and market analysts who need Portugal data that is actually usable — not four portals with four different field names.
Why this scraper
| This actor | Typical single-portal scrapers | |
|---|---|---|
| Portals | Imovirtual, OLX, CustoJusto, Idealista (via orchestration), SuperCasa (soon) | one |
| Schema | one ImovelListing for every portal | per-portal fields |
| Private vs. agency | native portal flags (isPrivateOwner, business, companyAd) — never guessed | usually missing |
| Phone | Imovirtual (agencies + private), OLX (via portal API), Idealista | varies |
| "Não aceito agências" | noAgenciesFlag — from the portal's own flag on CustoJusto, text detection elsewhere | — |
| Same property on 4 portals | recognized and marked (duplicateOf), or not delivered at all — and not charged | delivered 4 times, you clean it up |
| Phone numbers | all of them (phones[]) — agencies commonly publish two: office landline and agent mobile | the first one, if any |
| Price history & days on market | persistent across runs (priceHistory, priceChangePct, daysOnMarket) | — |
| Location | Portuguese admin hierarchy: district / municipality / parish, plus GPS when the portal has it | free text |
Input
{"portals": ["imovirtual", "olx", "custojusto"],"operation": "sale","propertyType": "apartment","locations": ["lisboa", "cascais", "porto"],"sellerType": "private","maxItemsPerPortal": 300,"fetchDetails": true,"priceHistory": true,"deduplicate": "drop"}
locations: district or municipality names, lowercase, no accents (lisboa,setubal,vila nova de gaia). Municipalities are resolved to each portal's own geography.startUrls: alternatively paste search-result URLs from any supported portal; the portal is detected from the domain.sellerType: "private"filters on the portal where possible and always on the output.fetchDetails: trueopens the listing page (phone, full description, energy certificate, year built). Doubles the requests — it is a separate, more expensive event.idealistaMode: "actor"(default) runs Idealista through a Store actor that uses the mobile API — far cheaper than a browser behind DataDome. Billed to your account by that actor."browser"uses the built-in Playwright adapter (needs residential proxy).
Output — one real item (phone masked)
{"id": "imovirtual:19268265","portal": "imovirtual","url": "https://www.imovirtual.com/pt/anuncio/apartamento-t3-com-estacionamento-e-vista-desafogada-sobre-o-rio-sob-ID1iQyt","operation": "sale","propertyType": "apartment","title": "Apartamento T3 com estacionamento e vista desafogada sobre o rio","price": 525000,"currency": "EUR","pricePerM2": 3454,"typology": "T3","bedrooms": 3,"bathrooms": 2,"areaM2": 152,"floor": "floor_3","yearBuilt": 2013,"energyCertificate": "A","condition": "used","district": "Lisboa","municipality": "Vila Franca de Xira","parish": "Alverca do Ribatejo e Sobralinho","locationRaw": "Rua do Bom Jesus, Sobralinho, Alverca do Ribatejo e Sobralinho, Vila Franca de Xira, Lisboa","latitude": 38.920174,"longitude": -9.024194,"sellerType": "agency","sellerName": "Century21 Nações","phone": "+3512189xxxxx","phones": ["+3512189xxxxx", "+3519123xxxxx"],"agencyLicense": "AMI 10786","noAgenciesFlag": false,"publishedAt": "2026-09-07","firstSeenAt": "2026-09-08T08:51:41.849Z","daysOnMarket": 1,"priceHistory": [{ "seenAt": "2026-09-08T08:51:41.849Z", "price": 525000 }],"priceChangePct": null,"duplicateOf": null,"duplicateOfPortal": null,"duplicateOfUrl": null,"images": ["https://ireland.apollo.olxcdn.com/v1/files/…/image;s=1280x1024;q=80"],"scrapedAt": "2026-09-08T08:51:41.849Z"}
Every field exists for every portal; what a portal does not publish comes back as null, never as a missing key. raw keeps the portal's original fields for anything not mapped.
Portal coverage (validated 2026-09-15)
| Portal | Source | Private/agency | Phone | Notes |
|---|---|---|---|---|
| Imovirtual | embedded JSON | native flag | agencies + private sellers | AMI licence, GPS, year built, energy |
| OLX | portal's public JSON API | native flag | yes, via the portal's phone endpoint | OLX limits phone reveals per session; the actor rotates sessions |
| CustoJusto | embedded JSON | native flag | no (portal requires login) | native noSalesmen ("no agencies") and previous price |
| Idealista | Store actor (orchestrated) | mapped | yes | 0,55 $/1 000 charged by that actor to your account |
| SuperCasa | — | — | — | behind a Cloudflare challenge; on the roadmap |
How often is there a phone
Measured on 2026-09-15, Lisbon, real runs — not an estimate:
| Portal | Listings with at least one phone | Notes |
|---|---|---|
| Imovirtual | ~100% of agency listings, and 4 in 6 published two numbers | the strongest source |
| OLX | ~15–30% | OLX allows roughly two phone reveals per IP before returning Disallowed for this user; the actor rotates sessions, so coverage rises with residential proxy and falls without |
| CustoJusto | 0% | the portal requires a login to show any number |
If phone coverage is what you are buying, run Imovirtual and Idealista, and treat OLX as a bonus. We would rather tell you this here than have you find out after paying.
Watch mode — only what is new
New listings are worth more than old ones. A private seller who posts today takes ten calls from
agencies within 48 hours; whoever reaches them first wins. So deliver lets you ask for the
delta instead of the whole market:
| Value | What you get |
|---|---|
all (default) | every listing found |
new | only listings this actor has never seen before |
new-or-changed | new ones, plus listings whose price moved since the last run |
Listings you already know are not delivered and not charged. Measured on 2026-09-15, OLX, Lisbon, private sellers only: a second pass examined 40 listings, delivered 8, and skipped 32 as already seen — the whole run cost $0.0035. Schedule it hourly and you get a handful of genuinely new ads per pass instead of re-importing the same market every time.
In watch mode maxItemsPerPortal caps listings examined, not delivered — otherwise the
actor would page through an entire portal hunting for new ads that may not exist yet.
Needs priceHistory: true; that is where the state between runs lives.
How many new listings are there, really
Measured the same day, OLX Lisbon, private sellers, for sale — the full set, not a sample:
| Window | New listings |
|---|---|
| last 24 hours | 6 |
| last 3 days | 21 |
| last 7 days | 48 |
| last 30 days | 163 |
Between 4 and 9 per day, steady across two weeks. That is the honest size of this feed for one district on one portal: small enough that one person can work every single one — which is the point.
The same property, four times
Portugal's portals overlap heavily. Run four single-portal scrapers over Lisbon and the same apartment arrives four times, with four ids and often four slightly different prices. Cleaning that up is the job you inherit.
This actor does it for you. deduplicate takes three values:
| Value | What happens |
|---|---|
mark (default) | every listing is delivered; repeats carry duplicateOf, duplicateOfPortal and duplicateOfUrl pointing at the first one |
drop | each property is delivered once — and the repeats are not charged |
off | no comparison at all |
Two listings are only merged when two independent pieces of evidence agree: same phone and same area (±2%); or same GPS to four decimals (~11 m) and same typology; or same municipality, typology, area (±2%) and price (±3%). A shared phone alone is not enough — one seller can own two flats. When in doubt it does not merge: an extra row is noise, a collapsed property is information you cannot get back.
Use cases
- Private-seller leads:
sellerType: "private"+fetchDetails: true→ name, phone, and whether the ad says "no agencies". Respect that flag. - Price-drop radar: schedule daily runs;
priceChangePctandpriceHistorytell you who lowered the price this week. - Market studies:
pricePerM2by parish, averagedaysOnMarket, share of private vs. agency listings. - CRM feed: run on a schedule, receive a webhook, or read the dataset by API (JSON / CSV / Excel).
Pricing
Pay per event — you pay per listing delivered, nothing else:
| Event | Price | When |
|---|---|---|
listing | $0.002 | one listing from search results (fetchDetails: false) |
listing-with-details | $0.005 | one listing with the detail page opened — phone, description, energy, year |
| Actor start | $0.005 / GB | per run |
Set a maximum spend per run in the Console; the actor stops cleanly when it is reached.
Proxies
Imovirtual, OLX and CustoJusto work with datacenter proxies (included). Idealista in browser mode needs a residential proxy with country PT; the actor enforces it.
Legal
Data comes from public listing pages. Seller names and phone numbers are personal data under the GDPR — you are responsible for the lawful basis of what you do with them, and for Portugal's rules on unsolicited commercial contact (Lei 41/2004). Honour noAgenciesFlag. Do not republish descriptions or photos.
Local development
npm install && npx playwright install chromiumnpm test # 12 tests, no browser needednpm run start:dev # reads storage/key_value_stores/default/INPUT.jsonapify push # deploy