# Imovirtual & Storia Property Scraper (Portugal, Romania) (`scrapyx/imovirtual-storia-properties-scraper`) Actor

Scrapes property listings from Imovirtual (Portugal) and Storia (Romania), the leading OLX Group real-estate portals in each market. Each row carries price, area, rooms, address, agency contact and photos, with optional full listing details.

- **URL**: https://apify.com/scrapyx/imovirtual-storia-properties-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.26 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Imovirtual & Storia Property Scraper — Portugal, Romania

Scrapes property listings from **Imovirtual** (Portugal) and **Storia**
(Romania), the leading OLX Group real-estate portals in each market — the
same platform family as `otodom-properties-scraper` (Poland), already in this
portfolio.

| Market | Site | Example scale (apartments for sale) |
| --- | --- | --- |
| Portugal | imovirtual.com | ~16,400 in Lisbon district, ~74,400 nationwide |
| Romania | storia.ro | ~22,700 in Bucharest, ~82,000 nationwide |

Residential and commercial, buy and rent, with optional full listing details.

***

### What each row contains

Every listing is emitted as upstream returns it. Highlights:

- **Price** (`totalPrice`), area, rooms, floor
- **Location** — full address text and coordinates
- **Advertiser** — agency name and logo, or private owner
- **Listing metadata** — creation date, image count, development/investment
  linkage for new-build units
- With `includePropertyDetails`: the full **description**, a structured
  **characteristics** table (price/m², build year, energy certificate),
  **features** by category, **owner contact details including phone
  numbers**, **breadcrumbs**, and **floor plans**

#### Record types

| `recordType` | One per | Purpose |
| --- | --- | --- |
| `SEARCH_SUMMARY` | query | upstream's own total, requests spent, honesty flags |
| `PROPERTY` | listing | the listing itself, upstream shape preserved |
| `ERROR` | failed input | so every input maps to at least one row |

***

### The one filter to be careful with

**Property type** is the dangerous input on this platform. An unrecognised
value is *not* rejected — it returns HTTP 200 with **every listing in the
whole country**, and it discards your location filter too. `searchApplied`
on the summary row reports whether this happened; when it did, the Actor
stops before crawling and emits **zero property rows** rather than filling
your dataset with the wrong data. This Actor validates the type against each
market's real list before ever sending the request, so this should not come
up in normal use — it exists as a safety net and as documentation for anyone
calling the underlying URL pattern directly.

Everything else offered here was individually verified to move the result
count: transaction, rooms, price range, area range and sort each narrow or
reorder genuinely, on **both** markets independently — Romania's default
sort already puts newest first, for instance, so "Newest first" is a no-op
there even though it works on Portugal.

***

### Input

| Field | Notes |
| --- | --- |
| `market` | `pt` (Imovirtual) or `ro` (Storia) |
| `locations` | Location slugs, e.g. `lisboa`, `porto`, `bucuresti`, `cluj`. Empty = whole country |
| `transaction` | `comprar`/`arrendar` (PT), `vanzare`/`inchiriere` (RO) |
| `propertyType` | See the market's allowlist in the input description |
| `rooms` | Multi-select, 1 through 10+ |
| `minPrice` / `maxPrice` | EUR |
| `minArea` / `maxArea` | m² |
| `sortBy` / `sortDirection` | Price, area, or newest |
| `pageSize` | 24, 36, 48 or 72 — anything else silently falls back to 36 |
| `includePropertyDetails` | One extra request per listing |
| `maxItems` | Per location |

***

### Known limits

- **No artificial pagination ceiling** — a whole-country apartment query pages
  through several hundred pages. Narrow by location, type or price for a
  quicker, more targeted crawl.
- **Pagination clamps past the last page** rather than erroring — page 458 of
  a 457-page result re-serves page 457. The Actor detects this (`no new ids`)
  and reports `paginationClampHit: true` rather than looping.
- **`totalMatches` is read from page 1 only.** The live index shifts under a
  running crawl.
- **otodom.pl (Poland) is not included here** — it already ships as its own
  Actor in this portfolio (`otodom-properties-scraper`), even though it runs
  the same platform.
- Listings removed mid-run answer 404 on the detail pass. The search row is
  still emitted, with `detailFetched: false`.

### Anti-bot

None encountered. Seven TLS fingerprints were tested against search pages
(page 1 and page 5) on both markets — all 28 combinations returned data
cleanly, cold, with no warmup. The Actor still rotates fingerprints and exit
IPs on failure, and defaults to residential proxy, because cloud egress is
fingerprinted differently from a local test.

# Actor input Schema

## `market` (type: `string`):

Which portal to scrape. Both run the same platform, so every option returns the same fields — only the language of the listings differs.

## `locations` (type: `array`):

One location **slug** per entry, each producing its own SEARCH\_SUMMARY row.

Portugal: `lisboa`, `porto`, `braga`, `faro`, `setubal`, `coimbra`. Romania: `bucuresti`, `cluj`, `timisoara`, `brasov`, `iasi`, `constanta`.

Slugs may be several segments deep for districts and parishes — copy the tail of any search URL on the site itself. **Leave empty to scrape the whole country** (`todo-o-pais` / `toata-romania`).

A location the portal does not recognise is answered with a clean 404 and reported as an ERROR row, so a typo can never silently return the wrong area.

## `transaction` (type: `string`):

Portugal: `comprar` (buy) or `arrendar` (rent). Romania: `vanzare` (buy) or `inchiriere` (rent).

Leave empty to use the market's default (buy). Validated before the run — an unrecognised value is refused rather than sent.

## `propertyType` (type: `string`):

Portugal: `apartamento`, `moradia`, `terreno`, `imoveis-comerciais`, `escritorio`, `armazens`, `garagem`, `quarto`. Romania: `apartament`, `garsoniere`, `casa`, `teren`, `spatiu-comercial`, `birou`, `depozite-hale`, `camera`.

Leave empty for the market's default (apartments).

**This is the one input the portal is dangerous about.** An unrecognised property type is *not* rejected — it returns HTTP 200 with every listing in the country **and silently discards your location too**. Values are therefore checked against the list above before the run, and the resolved URL is re-checked afterwards; if upstream ever widens the search anyway, the run stops and reports `searchApplied: false` instead of returning the wrong rows.

## `rooms` (type: `array`):

Match any of the selected room counts. Leave empty for any.

Verified real: 3 rooms narrowed Lisbon apartments from 16,418 to 6,036 and Bucharest apartments from 22,750 to 6,986.

## `minPrice` (type: `integer`):

Verified real: `300000` narrowed Lisbon apartments from 16,418 to 14,205. Leave at 0 to skip.

## `maxPrice` (type: `integer`):

Leave at 0 to skip.

## `minArea` (type: `integer`):

Verified real: `100` narrowed Lisbon apartments from 16,418 to 8,476. Leave at 0 to skip.

## `maxArea` (type: `integer`):

Leave at 0 to skip.

## `sortBy` (type: `string`):

Proven by checking that results genuinely reorder, not merely that the request succeeded — an unrecognised sort is silently ignored by this platform.

Note: `Newest first` reorders Portugal but is a no-op on Romania, whose default ordering is already newest-first.

## `sortDirection` (type: `string`):

Only meaningful together with a sort field.

## `pageSize` (type: `string`):

Only these four are honoured. Anything else (100, 200) is silently ignored and falls back to 36, so the list is closed. 72 halves the number of requests versus the default.

(Carried as a string because the platform does not allow a dropdown on an integer field; the value is a plain number of listings either way.)

## `includePropertyDetails` (type: `boolean`):

Fetch each listing's own page for the ~50 fields the results list does not carry: the full description, characteristics (price per m², build year, energy certificate), the complete feature list, advertiser contact details including phone numbers, breadcrumbs and floor plans.

Costs **one extra request per listing**. De-listed ads return 404 — those rows are still emitted, with `detailFetched: false`.

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

Stop after this many listings per location. Set to 0 for everything the portal will serve — there is no artificial ceiling here, so a whole-country query can run to hundreds of pages.

## `maxConcurrency` (type: `integer`):

Upper bound on requests in flight at once, across all locations and detail fetches.

## `minRequestInterval` (type: `integer`):

Paces how often requests START, without tying up a concurrency slot. No rate limiting was observed, so this defaults to 0; raise it for long unattended crawls.

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

Residential by default. No bot challenge appeared on any of the 7 TLS fingerprints tested across both markets, but datacentre egress from a cloud platform is fingerprinted differently from a local test — an earlier actor in this portfolio lost every request that way.

## Actor input object example

```json
{
  "market": "pt",
  "locations": [
    "lisboa"
  ],
  "transaction": "",
  "propertyType": "",
  "rooms": [],
  "minPrice": 0,
  "maxPrice": 0,
  "minArea": 0,
  "maxArea": 0,
  "sortBy": "",
  "sortDirection": "",
  "pageSize": "72",
  "includePropertyDetails": false,
  "maxItems": 100,
  "maxConcurrency": 4,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "locations": [
        "lisboa"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/imovirtual-storia-properties-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 = { "locations": ["lisboa"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/imovirtual-storia-properties-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 '{
  "locations": [
    "lisboa"
  ]
}' |
apify call scrapyx/imovirtual-storia-properties-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/imovirtual-storia-properties-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/fVgHWHsqqfAXgDgRm/builds/BBlKVU1U41RsZuMAQ/openapi.json
