DEV Community

Cover image for How to Scrape Immobiliare.it Listings with Python and Apify
ReventaData
ReventaData

Posted on Originally published at apify.com

How to Scrape Immobiliare.it Listings with Python and Apify

By the end of this tutorial you will have a Python script that pulls Italian property listings from Immobiliare.it into structured Python dictionaries: asking price or monthly rent in euros, surface area in square metres, rooms, floor, coordinates, feature tags, and the agency and agent contact details published on the ad. We use the Apify Actor Immobiliare.it Scraper (reventadata/immobiliare-scraper) and the official Apify Python client, so you do not need to run a browser, configure proxies, or parse HTML yourself.

If you are looking for a practical way to scrape Immobiliare.it across cities, provinces, or regions, this guide walks through configuring a rental search in Torino, inspecting the returned fields, and ranking rentals by monthly rent per square metre.

⏱️ Time estimate: Setup takes a few minutes.

This Actor is unofficial and not affiliated with Immobiliare.it.

Prerequisites

You need three things before running the script:

  1. Python 3.11 or newer (apify-client 3.x requires Python 3.11+).
  2. A free Apify account and your personal API token from the API & Integrations page in Apify Console.
  3. The official Apify Python client library installed in your environment:
pip install -U "apify-client~=3.0"
Enter fullscreen mode Exit fullscreen mode

Export your API token in your shell so it stays out of your source code:

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

What you'll achieve

You will run Immobiliare.it Scraper from Python and receive one dictionary per property ad, ready to filter in Python or export as JSON, CSV, or Excel. Each record includes numeric and formatted prices, price reduction details when a listing has been discounted, room and bathroom counts, numeric floor area in square metres (surfaceM2), street address, neighbourhood, city, province, region, latitude and longitude, photo URLs, and public seller contact fields (agencyName, agencyPhone, agentName, agentPhone).

As a concrete example, we will search Torino rentals between 500 and 900 EUR per month with at least two rooms, then rank sample rows by monthly rent per square metre (price / surfaceM2) to spot the lowest cost per square metre.

Step 1: Open the Actor

Go to the Immobiliare.it Scraper page on Apify Store to review the input schema and documentation. Because we trigger the run from Python, you do not need to fill in the web form.

Step 2: Configure the search parameters

The Actor accepts the following input fields, sending your filter parameters directly in the immobiliare.it search URL:

Field Type Default What it does
contract string "sale" "sale" (vendita-case) or "rent" (affitto-case).
location string "torino" Location slug from an Immobiliare.it search URL, such as "torino", "milano", "torino-provincia", or "lombardia".
minPrice, maxPrice integer unset Optional price limits in EUR (prezzoMinimo, prezzoMassimo). On rent searches this is the monthly rent.
minSurface integer unset Optional minimum surface in m² (superficieMinima).
minRooms, maxRooms integer unset Optional room (locali) limits (localiMinimo, localiMassimo).
sortBy string "relevance" "relevance" (site default) or "priceAscending" (criterio=prezzo&ordine=asc).
maxItems integer 1000 Maximum number of rows pushed to the dataset.
maxPages integer 80 Maximum result pages fetched (25 ads per page), capped at the site ceiling of 80.

Both contract and sortBy are enum fields in the input schema: any value outside their allowed choices is refused before the run starts. To find a location slug, run a search on immobiliare.it and copy the path segment after /vendita-case/ or /affitto-case/.

Here is the first part of our Python script, defining a rental search in Torino between 500 and 900 EUR per month with at least 2 rooms:

import os
from apify_client import ApifyClient

# 1. Initialize the client with your token from the environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])

# 2. Describe the search: rent in Torino, 500 to 900 EUR/month, at least 2 rooms
run_input = {
    "contract": "rent",
    "location": "torino",
    "minPrice": 500,
    "maxPrice": 900,
    "minRooms": 2,
    "sortBy": "relevance",
    "maxItems": 20,
    "maxPages": 1,
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Run the Actor

Call the Actor with client.actor("reventadata/immobiliare-scraper").call(run_input=run_input). It starts the run on Apify, waits for completion, and returns the settled run object:

# 3. Start the Actor and wait for it to finish
run = client.actor("reventadata/immobiliare-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. Read the rows from the run's default dataset
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} listings.")
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, the Actor fetches each search page through a third-party unblocking service that the Actor pays for (no proxy configuration or key of your own is required from you) and parses the <script id="__NEXT_DATA__"> payload (real-estate-list query).

Step 4: Print the first rows

Iterate through the dataset items to inspect titles, prices, surface area, rooms, floor, neighbourhood, and seller contact details:

for item in items[:3]:
    price_text = item["priceText"] or "price on request"
    surface_text = item["surfaceText"] or "surface n/a"
    seller = item["agencyName"] or item["agentType"] or "seller n/a"
    print(item["title"])
    print(f'  {price_text} | {surface_text} | rooms: {item["rooms"]} | floor: {item["floor"]}')
    print(f'  {item["neighbourhood"]}, {item["city"]} | {seller} ({item["agencyPhone"] or "no phone"})')
    print(f'  {item["url"]}')
Enter fullscreen mode Exit fullscreen mode

Step 5: Inspect a sample row

Below is one record from the sample rows from a run on 2026-11-10 using the Torino rental input above (photos is shortened here from 44 thumbnail URLs to the first two):

{
  "id": "122075776",
  "url": "https://www.immobiliare.it/annunci/122075776/",
  "title": "Trilocale via Francesco Cigna 44, Aurora, Torino",
  "contract": "rent",
  "adType": "ad",
  "price": 800,
  "priceText": "€ 800/mese",
  "priceVisible": true,
  "priceOriginalText": "€ 1.010",
  "priceReductionPercent": 20.8,
  "priceReducedOn": "29/10/2026",
  "typology": "Trilocale",
  "category": "Residenziale",
  "isProject": false,
  "projectUnits": null,
  "isNew": false,
  "isLuxury": false,
  "rooms": "3",
  "bedrooms": 2,
  "bathrooms": "1",
  "surfaceText": "85 m²",
  "surfaceM2": 85,
  "floor": "3",
  "floorText": "3°",
  "hasElevator": null,
  "condition": "Ottimo / Ristrutturato",
  "heating": "Centralizzato",
  "features": [
    "fibra ottica",
    "porta blindata",
    "balcone",
    "impianto tv centralizzato",
    "arredato",
    "esposizione doppia"
  ],
  "caption": "AFFITTASI TRILOCALE RISTRUTTURATO VICINO AL CENTRO DI TORINO",
  "description": null,
  "address": "Via Francesco Cigna",
  "zone": "Aurora, Barriera di Milano, Rebaudengo",
  "neighbourhood": "Aurora",
  "city": "Torino",
  "province": "Torino",
  "region": "Piemonte",
  "latitude": 45.0836,
  "longitude": 7.6796,
  "imageUrl": "https://pwm.im-cdn.it/image/1754187330/xxl.jpg",
  "photoCount": 44,
  "photos": [
    "https://pwm.im-cdn.it/image/1754187330/xxs-c.jpg",
    "https://pwm.im-cdn.it/image/1754187342/xxs-c.jpg"
  ],
  "agencyId": 197355,
  "agencyName": "Studiocasa",
  "agencyType": "agenzia",
  "agencyPhone": "011 1980 1344",
  "agencyPhones": [
    "011 1980 1344"
  ],
  "agencyUrl": "https://www.immobiliare.it/agenzie-immobiliari/197355/studio-casa-torino/",
  "agentName": "Guglielmo Marino",
  "agentType": "agente immobiliare",
  "agentPhone": "011 1980 1344",
  "agentPhones": [
    "011 1980 1344"
  ],
  "transactionDate": null,
  "searchUrl": "https://www.immobiliare.it/affitto-case/torino/?prezzoMinimo=500&prezzoMassimo=900&localiMinimo=2"
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Rank rentals by monthly rent per square metre

Because each row provides numeric price and surfaceM2, we can rank single-unit listings by monthly rent per square metre (price / surfaceM2), skipping new-development project tiles (isProject) and unpriced ads:

# 5. Rank rentals by monthly rent per square metre (EUR/m2)
ranked = [
    item for item in items
    if item["price"] is not None and item["surfaceM2"] and not item["isProject"]
]
ranked.sort(key=lambda i: i["price"] / i["surfaceM2"])
print("Cheapest per m2:")
for item in ranked[:5]:
    per_m2 = item["price"] / item["surfaceM2"]
    seller = item["agencyName"] or item["agentType"] or "n/a"
    print(
        f'{per_m2:5.1f} EUR/m2 | {item["price"]:4d} EUR | '
        f'{item["surfaceM2"]:3d} m2 | rooms: {item["rooms"]} | '
        f'{item["neighbourhood"]} | {seller}'
    )
Enter fullscreen mode Exit fullscreen mode

Running this ranking snippet over the sample rows from a run on 2026-11-10 prints:

Cheapest per m2:
  9.4 EUR/m2 |  800 EUR |  85 m2 | rooms: 3 | Aurora | Studiocasa
 10.0 EUR/m2 |  600 EUR |  60 m2 | rooms: 2 | Don Bosco | Studiocasa
 10.0 EUR/m2 |  750 EUR |  75 m2 | rooms: 3 | Aurora | Studiocasa
 13.3 EUR/m2 |  600 EUR |  45 m2 | rooms: 2 | Parella | Immobiliare Michielin
 13.5 EUR/m2 |  850 EUR |  63 m2 | rooms: 3 | Cittadella | RE/MAX Vivere
Enter fullscreen mode Exit fullscreen mode

Understanding your results

Each dataset row contains 53 fields declared in the Actor's dataset schema:

  • price, priceText, priceVisible: integer price in EUR (null when "prezzo su richiesta"), formatted string, and visibility boolean.
  • priceOriginalText, priceReductionPercent, priceReducedOn: populated when the listing displays a price reduction.
  • surfaceM2, surfaceText, rooms, bedrooms, bathrooms, floor, floorText, hasElevator: size and layout from properties[0]. rooms and bathrooms stay strings for values like "5+" or "2 - 5+"; bedrooms is an integer only when the site gives a single number.
  • agencyName, agencyType, agencyPhone, agencyPhones, agentName, agentType, agentPhone: public seller details (null name and phone when agentType is "privato").

New-development project tiles (isProject: true) group several units. Here is an excerpt from a Torino sale capture dated 2026-11-07:

{
  "id": "97492960",
  "title": "Appartamenti e Attici di nuova costruzione a Torino",
  "contract": "sale",
  "price": 298000,
  "priceText": "€ 298.000 - € 1.009.000",
  "typology": "Progetto",
  "category": "Nuove costruzioni",
  "isProject": true,
  "projectUnits": 10,
  "rooms": "2 - 5+",
  "bedrooms": null,
  "surfaceText": "66 m²",
  "surfaceM2": 66,
  "agencyName": "Residenza PARCO SUL PO",
  "agencyType": "Impresa Edile",
  "agencyPhones": [
    "011 771 5000",
    "366 509 2054"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Pro tips

  1. Plan around the 80-page site ceiling: Immobiliare.it serves at most 80 pages (2,000 ads, at 25 per page) per search query; requesting page 81 returns HTTP 404 on the site even when totalAds is larger (such as 8,662 sale ads in Torino on 2026-11-07). When totalAds exceeds 2,000, the Actor logs a warning on page 1. Split broad searches by minPrice/maxPrice, minSurface, or minRooms/maxRooms.
  2. Start with a small page cap: The Actor fetches pages one after another, so a many-page run takes longer. Keep maxPages: 1 and maxItems: 20 while testing your pipeline.
  3. Send filters in the search URL: minPrice, maxPrice, minSurface, minRooms, maxRooms, and sortBy: "priceAscending" are applied by Immobiliare.it on the server before rows are returned.
  4. Separate auctions and project tiles in analysis: Sale searches can return property auctions (contract: "auction", adType: "auction") and multi-unit project tiles (isProject: true). Filter on adType == "ad" and not isProject when comparing standard listings.

Troubleshooting

  • Run succeeded with 0 rows: When page 1 loads with totalAds: 0 and empty results, nothing matched on the site. The run ends SUCCEEDED with a status message; loosen your price, surface, or room limits.
  • Run failed on page 1 with a location error: An HTTP 404 on page 1 means the location slug does not exist on immobiliare.it and ends the run FAILED naming the slug. Copy the exact slug from a real search URL.
  • Run failed before any request: Passing minPrice > maxPrice, minRooms > maxRooms, negative numbers, or a malformed slug fails immediately. Values outside the enums for contract or sortBy are refused before the run starts.
  • Reading the log: Call client.log(run.id).get() to inspect Skipped N: already pushed (ids repeat across pages) and Pushed N row(s)., or check the Store page.

Next steps

For current pricing and details, see the Store page.

To monitor a city or price band over time, save your input configuration as an Apify task and attach an Apify schedule on a cron expression.

Which Italian cities or property segments are you planning to analyze? Let us know in the comments!

Top comments (0)