Complete Guide: How to Scrape Property Data Using Imovirtual Scraper
Run the Imovirtual Scraper Apify Actor from Python and get Portuguese property listings with price, price per m², area and parish as JSON rows.

This complete guide to Imovirtual Scraper shows how to scrape Portuguese property listings from Imovirtual with Python through the Apify API, and get back one typed row per listing: title, asking price or monthly rent in euros, price per m², area, rooms, floor, the address down to parish and street, the agency or developer, and when the listing was created and last bumped.
Imovirtual (imovirtual.com) is a Portuguese real-estate portal covering apartments, houses, land, commercial property, warehouses and garages, for sale or for rent. Its search pages are built for people browsing cards. For a spreadsheet, a price-per-m² comparison or a feed of new listings, you want the same data as rows.
You will configure the Imovirtual Scraper Apify Actor (reventadata/imovirtual-scraper), run it from Python, read its rows and use its filters. Prerequisites: Python 3.11+, the apify-client package and an Apify API token. Expect one sitting. The sample row in this guide is a sample from a capture dated 2026-10-20, parsed with the Actor's own parser; this guide quotes no run of its own.
TL;DR
In this tutorial, you'll learn how to scrape Imovirtual with the Imovirtual Scraper Apify Actor from Python. Key steps: install apify-client, export APIFY_API_TOKEN, choose transaction, propertyType and a location slug, optionally narrow by typology, price, area or private owners, call reventadata/imovirtual-scraper, and read the rows from the run's default dataset. Every filter goes into Imovirtual's own search URL.
Expected outcome: a JSON list of listings with price, pricePerSquareMeter, areaM2 and the parish, council and district of each, ready for a spreadsheet or database.
The Actor: apify.com/reventadata/imovirtual-scraper.
Prerequisites and setup
You will need:
- Python 3.11+ (
apify-client3.3.0 requires it). - The Apify API client for Python.
- An Apify account. Apify Console is Apify's web interface; your API token is on its API & Integrations page. The platform documentation starts at docs.apify.com.
Install the client:
pip install "apify-client~=3.0"
The token is a secret. Apify's docs advise giving each token a description and never using one token for several services. The code in this guide reads it from the environment, so it never sits in a script or a commit.
Set the token in your environment:
export APIFY_API_TOKEN="your_apify_token_here"
Understanding Imovirtual Scraper
What it does and how it works
- URL building. From your input the Actor builds the search URL Imovirtual's own interface produces:
/pt/resultados/{comprar|arrendar}/{type}[,t{n}]/{location}, withpriceMin,priceMax,areaMin,areaMax, owner type and sort order as query parameters, and?page=Nfrom page 2 on. An empty numeric filter is left out, never sent as 0. - Transport. Plain requests, no proxy by default. Requests with no proxy reached imovirtual.com when the Actor was tested on 2026-10-20.
- Parsing. Imovirtual embeds the search results as JSON in its own page. The Actor reads that data, so no browser is involved. Each page holds 36 listings; page 1 adds a premium tile, repeated on later pages, which the Actor saves once.
- Stopping. The walk ends at
maxItems, atmaxPages, or at the last page the site reports for the search.
Limitations
- Typology goes up to
t5;sortByoffers relevance, cheapest first and newest, not most expensive first. - A row carries no phone number or e-mail address; the listing
urlleads to the ad page. - The Actor reads search pages only, never the individual ad pages, so it returns what the search result carries.
Step-by-step walkthrough
Step 1: Initialize the client and define inputs
Three inputs define the search. transaction is buy or rent. propertyType is one of apartamento, moradia, terreno, imoveis-comerciais, armazens or garagem. location is Imovirtual's slug from a search URL: a district (lisboa, porto), a council (lisboa/lisboa, porto/porto) or todo-o-pais for the whole country. maxItems (default 1000) caps saved rows and maxPages caps result pages.
This setup searches apartments for sale in Lisboa district and reads one result page:
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 Lisboa district, one result page
run_input = {
"transaction": "buy",
"propertyType": "apartamento",
"location": "lisboa",
"maxPages": 1,
}
Two more configurations, as JSON input:
- Lisbon city T2 flats under 400,000 EUR:
{"transaction": "buy", "propertyType": "apartamento", "location": "lisboa/lisboa", "typology": "t2", "maxPrice": 400000, "maxItems": 200}(the README's sample input). - Porto rentals, cheapest first:
{"transaction": "rent", "propertyType": "apartamento", "location": "porto/porto", "sortBy": "priceAsc", "maxPages": 2}.
To find a slug, search on imovirtual.com and copy the URL part after the property type.
Step 2: Configure filters
Every filter is sent to Imovirtual in the URL:
typology:t0tot5, the Portuguese bedroom count.minPrice/maxPrice: price band in EUR (monthly rent on rent searches).minArea/maxArea: area band in m².ownerType:any, orprivatefor private owners only.sortBy:relevance,priceAscornewest.
Measured on 2026-10-20 for apartments for sale in Lisboa district: maxPrice: 300000 cut the site's count to 2,158 listings, and private owners only to 174, against 17,210 with no filter.
A typology, sortBy, ownerType, transaction or propertyType outside its list is refused before the run starts.
Step 3: Choose the scope
location sets the scope, and a district is wider than it sounds. In the capture, page 1 of the lisboa search spanned 8 councils: 18 rows in Lisboa itself, 6 in Loures, 5 in Vila Franca de Xira, and the rest in Sintra, Odivelas, Oeiras, Mafra and Amadora. Use lisboa/lisboa for the city only.
Step 4: Run the Actor
.call() starts the run and waits for it to finish:
# 3. Start the Actor run and wait for it to finish
print("Starting Imovirtual extraction...")
run = client.actor("reventadata/imovirtual-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")
Step 5: Process the rows
seen_urls = set()
for item in items:
# Skip a second id that points to a listing already printed
if item["url"] in seen_urls:
continue
seen_urls.add(item["url"])
print(item["title"])
price = item.get("price")
print(f" Price: {price:,.0f} EUR" if price is not None else " Price: not published")
print(f" Per m2: {item.get('pricePerSquareMeter')} EUR, area {item.get('areaM2')} m2")
print(f" Rooms: {item.get('roomsLabel')}, floor {item.get('floor')}")
print(f" Where: {item.get('parish')}, {item.get('council')}")
print(f" Seller: {item.get('agencyName') or 'private owner'}")
print("-" * 40)
Step 6: Inspect a real row
A sample from a capture dated 2026-10-20 (page 1 of the search above), parsed with the Actor's own parser:
{
"id": "19316271",
"url": "https://www.imovirtual.com/pt/anuncio/apartamento-t3-a-venda-na-praca-das-novas-nacoes-arroios-ID1j32L",
"title": "Apartamento T3 à venda na Praça das Novas Nações, Arroios",
"propertyType": "FLAT",
"transaction": "SELL",
"price": 555000.0,
"currency": "EUR",
"pricePerSquareMeter": 6686.75,
"priceFromPerSquareMeter": null,
"areaM2": 83.0,
"rooms": 4,
"roomsLabel": "FOUR",
"floor": "SECOND",
"street": "Praça das Novas Nações",
"neighborhood": "Anjos",
"parish": "Arroios",
"council": "Lisboa",
"district": "Lisboa",
"agencyName": "Century 21 CasaBela",
"agencyType": "AGENCY",
"advertiserName": "Century 21 CasaBela",
"contactNames": [
"Sofia Dias"
],
"isPrivateOwner": false,
"isPromoted": false,
"isExclusiveOffer": false,
"createdAt": "2026-10-20T18:14:18+00:00",
"pushedUpAt": null,
"imageUrl": "https://ireland.apollo.olxcdn.com/v1/files/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmbiI6Ijd0a3lyMXJxdTdnYjMtRUNPU1lTVEVNIiwidyI6W3siZm4iOiI2OW5scGE3ZWNxbTcxLUFQVCIsInMiOiIxNiIsImEiOiIwIiwicCI6IjEwLC0xMCJ9XX0.YzJy2krk122zcGgyU5_lDEsJbUD59FTBaXjmIhte5Qk/image;s=1280x1024;q=80",
"searchUrl": "https://www.imovirtual.com/pt/resultados/comprar/apartamento/lisboa"
}
Step 7: Export
Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL, from Console or through the API's format parameter.
Working with the API
The Python client gives two ways to start this Apify Actor. client.actor("reventadata/imovirtual-scraper").call(run_input=run_input) starts the run and waits for it to finish before returning the run object, or None; that is what the code above uses. For long runs or a job queue, .start(run_input=run_input) returns the run object at once, and you read it back later with its ID.
Authentication is the API token in APIFY_API_TOKEN; keep it out of source control. Once a run finishes, client.dataset(run.default_dataset_id).list_items().items returns the rows in one call, and iterate_items() pages through them for larger datasets, with a fields argument to fetch only the columns you need.
For event-driven pipelines, attach a webhook for the ACTOR.RUN.SUCCEEDED event. Its payload's resource is the response of the Get Actor run API endpoint, so it carries defaultDatasetId, and your handler can fetch the rows without polling.
Error handling: check run.status as the code does. The status is SUCCEEDED, FAILED, TIMED-OUT or ABORTED. A run that saves no rows ends FAILED when it is blocked or cannot read a page, and a run with bad input ends FAILED too; a search that loads normally but matches nothing ends SUCCEEDED with an empty dataset and a status message saying so. When a run fails, read its log from Python with client.log(run.id).get(). A slug Imovirtual does not know ends the run with a 404 error that says so.
Limits: the account's memory and concurrent-run limits apply to every start. call() accepts wait_for_resources, which keeps retrying the start while the API rejects it for either limit.
Advanced usage patterns
Several searches
One run covers one search. To compare places or property types, start one run per search, for example one per council slug, and tag each dataset by its searchUrl, which every row carries.
Scheduled runs
Save the input as a task and add an Apify schedule. Schedules run an Actor or task on a cron expression; 0 8 * * * is every day at 8 AM. With sortBy: "newest" and a small maxPages, each run reads the most recently listed properties first.
Monitoring
Combine the schedule with a webhook on ACTOR.RUN.SUCCEEDED to load each new dataset into your database, and treat a FAILED run as a signal: with this Actor it means a wrong slug, or that no rows were saved because of a block or a page the parser no longer recognises. An over-strict filter ends SUCCEEDED with an empty dataset, so check the item count or the status message for that. The run log says which.
Volume control
maxItems and maxPages are the two bounds. Without maxPages the walk runs to the last page the site reports: 479 pages for apartments for sale in Lisboa district on 2026-10-20. Because filters go into the URL, narrowing by price, area or typology shrinks the page count itself rather than discarding rows after the fetch.
Understanding your data
Key fields and how to read them:
price,currency: EUR on every captured row; the monthly rent on rent searches. Development listings carryprice: nulland a "from"priceFromPerSquareMeter; none of the 37 captured rows did.pricePerSquareMeter,areaM2: from 1,446.95 to 6,875 EUR per m² and 82 to 1,866 m² in the capture.rooms,roomsLabel: rooms, not bedrooms. The sample is a T3 withroomsLabelFOUR.MOREhas no number, soroomsis null there; 4 captured rows.agencyType,isPrivateOwner: 31AGENCY, 5DEVELOPERand 1 private owner in the capture.agencyNameis null for a private owner;advertiserNamethen holds the name the listing shows.createdAt,pushedUpAt: first listed and last bumped; 22 captured rows had been bumped.- Duplicates: one listing appeared under a second id (
9000062240) with the sameurl, no image and acreatedAtof1999-02-29 00:00:01, not a real date. Deduplicate onurland parse dates defensively.
Troubleshooting guide
- Zero rows:
FAILEDmeans a wrong slug, a block or an unreadable page;SUCCEEDEDwith an empty dataset means the pages loaded but nothing matched. Widen the price or area band, droptypology, or setownerTypeback toany. - Run ends at once with a 404 error: the
locationslug does not exist on Imovirtual. Copy it from a real imovirtual.com search URL. - Run fails before any fetch: the slug is not slug-shaped (spaces,
?or other characters). Use lowercase words joined by hyphens and slashes, as inporto/porto. - Input refused before the run starts: a value outside a select list, such as
typology: "t6"orsortBy: "priceDesc". - Suburbs in a city search:
lisboais a district; uselisboa/lisboa. - Rent prices look tiny: on rent searches
priceis the monthly rent. - Reading the log:
client.log(run.id).get(); it reportsPage 1 returned N items of N pages.,Skipped N: ...per reason, and a summary line near the end,Pushed N row(s).. A page that keeps failing is logged and skipped, and the run goes on.
For help, use the Actor's Store page.
Best practices
- Filter in the URL. Price, area, typology and owner type are applied by Imovirtual, so they shrink the number of pages read instead of dropping rows after the fetch.
- Use council slugs for cities.
lisboa/lisboa,porto/porto. - Always set a bound.
maxPagesor a lowermaxItems; the defaultmaxItemsis 1000, and withoutmaxPagesthe walk goes to the site's last page. - Start small.
maxPages: 1until your code handles the fields. - Deduplicate on
url, not onlyid. - Mind freshness. Listings change and ads get bumped; schedule reruns instead of reusing old datasets, and compare
pushedUpAtbetween runs. - Handle failure explicitly. Check
run.statusbefore reading rows.
Pricing
For current pricing and details, see the Store page.
Conclusion and next steps
You configured Imovirtual Scraper, ran it from Python and read typed listing rows with prices, areas and addresses, filtered by Imovirtual itself. Next: schedule a newest search, attach a webhook, or run one search per council and compare price per m².
Try Imovirtual Scraper yourself and let me know what you build!
About the author: reventadata builds and maintains marketplace and property scrapers on the Apify Store.
Resources
About the author
- Hi, I’m the engineer behind ReventaData. I build automated web scrapers, reverse-engineer undocumented APIs, and design data pipelines on Apify that turn complex, unstructured websites into clean, reliable feeds. Here on Hashnode, I share practical tutorials on modern web scraping with Python, bypassing aggressive anti-bot protections (Cloudflare, Akamai, PerimeterX), and data pipeline automation, reverse-engineering hidden endpoints, and automating data extraction. Explore our turnkey scrapers: https://apify.com/reventadata
Comments
Join the discussionNo comments yet. Be the first to comment.




