# Idealista Scraper: Real Estate Price History & Removed Listings (`moxlade/idealista-scraper`) Actor

Idealista listings with their history, an idealista API for real estate data in Spain, Italy and Portugal: first seen, days on market, price history and the day idealista removed each listing. Rental and sale searches by URL or place name, details and photos, listing watch. No login.

- **URL**: https://apify.com/moxlade/idealista-scraper.md
- **Developed by:** [Moxlade](https://apify.com/moxlade) (community)
- **Categories:** Real estate, Automation, Lead generation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 listing row (from the corpus)s

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Idealista Scraper: Real Estate Price History & Removed Listings

**idealista listings that remember.** Give it an idealista search page (idealista.com, idealista.it or idealista.pt) or a place name, and get one row per listing with the card's fields — price, size, rooms, floor, the agency or private flag, the position in idealista's own list, idealista's own count — plus what no page shows: when the listing was first seen, how long it has been listed, every price it has had, and for a listing that went away, the day idealista removed it. Ask for a listing's page and get the full description, every photo with its room tag, the floor plans, the energy letters, the coordinates and the agency's phone. Watch listing URLs: live, or removed with the date. Ask an area for the listings it lost in a window. No login, no proxy to bring, no private person's name or phone, ever.

### Barcelona rentals, with their history

Give it this:

```json
{
  "searchUrls": [
    "https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc"
  ],
  "maxItems": 100
}
```

Back comes one row per listing (`status` says whether it is live or removed; a free row says why there is none) — the fields below are the ones this real row carried; every row has the same 74, and a field the source did not show arrives as null, not as a zero:

```json
{
  "row_type": "listing",
  "input": "https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc",
  "input_index": 1,
  "status": "ok",
  "scraped_at": "2026-10-14T22:55:26Z",
  "read_from": "live",
  "read_at": "2026-10-14T22:55:26Z",
  "id": "112807204",
  "url": "https://www.idealista.com/inmueble/112807204/",
  "market": "es",
  "operation": "rent",
  "property_type": "homes",
  "title": "Piso en Calle dels Madrazo, Sant Gervasi - Galvany, Barcelona",
  "price": 2250,
  "currency": "EUR",
  "price_unit": "month",
  "size_m2": 125,
  "rooms": 4,
  "floor": 5,
  "floor_text": "5ª planta exterior con ascensor",
  "is_exterior": true,
  "has_lift": true,
  "tags": ["Garaje incluido"],
  "published_ago": "1 hora",
  "thumbnail_url": "https://img4.idealista.com/blur/480_360_mq/0/id.pro.es.image.master/ec/01/fc/1482998693.jpg",
  "photos_count": 18,
  "advertiser_type": "agency",
  "search_url": "https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc",
  "search_total": 2277,
  "page": 1,
  "rank": 2,
  "first_seen_at": "2026-10-14T22:55:26Z",
  "last_seen_at": "2026-10-14T22:55:26Z",
  "days_listed": 0,
  "price_history": [{"at": "2026-10-14T22:55:26Z", "price": 2250}],
  "price_changes_count": 0,
  "is_offmarket": false
}
```

The same actor, asked differently:

- **A place by name: homes for sale in Sitges** — `{"location": "Sitges", "country": "es", "operation": "sale", "propertyType": "homes", "maxItems": 50}`
- **Milan rentals with every detail, photos and the agency's phone** — `{"searchUrls": ["https://www.idealista.it/affitto-case/milano-milano/?ordine=pubblicazione-desc"], "includeDetails": true, "maxItems": 30}`
- **Watch listings: live, or removed with idealista's date** — `{"listingUrls": ["https://www.idealista.com/inmueble/112816892/", "https://www.idealista.com/inmueble/108000000/"]}`
- **What Barcelona lost since 1 October 2026 (gone mode)** — `{"searchUrls": ["https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc"], "removedSince": "2026-10-01", "maxItems": 500}`

### What a row carries

**The history no page shows.** Every row carries `first_seen_at`, `last_seen_at`, `days_listed`, `price_history` (every price our reads saw, with the date), `price_previous`, `price_change` and, for a listing that went away, `removed_on` — idealista's own date, from the listing's own page — next to the day a sweep found it gone. The history comes from a corpus shared by every run: a listing another buyer's run read last week arrives with last week's price in its history.

**Two ways to answer, both said on the row.** A search whose area was swept whole in the last `maxAgeHours` (24 by default) is answered from the corpus at once: `read_from: corpus`, `read_at` the sweep's time, no browser, no waiting. Otherwise idealista is read live — one browser load to open the exit, then the pages at idealista's own pace — the corpus is updated, and the rows say `read_from: live`. Set `maxAgeHours: 0` to read live every time.

**The search as idealista shows it.** Any search page of the three hosts is the input — the place, the operation, the property type and the filters as the site's own URL carries them — and the walk follows the page's own next link to the last page: no cap of 1,500 or 2,500, no silent cut. `search_total` is idealista's own count of the hour on every row, `rank` the listing's position in the list, and the run summary says COMPLETE, TRUNCATED (your cap) or PARTIAL (a time limit) for each search. On idealista.it and idealista.pt the listings come in the order the URL asks for (`?ordine=pubblicazione-desc`, `?ordem=atualizado-desc` = newest first). idealista.com refuses its sorted pages to this route, so a .com search comes in idealista's default (relevance) order and the run summary says so; for the newest .com listings, `onlyNew` on a saved task delivers each new listing once whatever the order. A card that shows a price drop carries idealista's own former price and the drop in percent.

**A place name works too.** `location` with `country`, `operation` and `propertyType` is resolved by idealista's own search suggestions to its first match (`Barcelona`, `Sitges`, a postal code such as `08001`); the log says which search URL answered.

**The listing's page when you ask for it.** `includeDetails` reads each listing's own page: the full description, every photo with its room tag (the page's own size, unsigned CDN links that do not expire), the floor plans, the three feature lists with furnished / terrace / parking / air-conditioning / pool flags, bathrooms, the energy letters, the coordinates, the reference, idealista's location id, the day idealista last updated the listing, with `descriptionLanguage` the description in one of 18 languages from the page's own language switch, and the agency's phone from idealista's own contact call — agency listings only. idealista refuses that phone call on some exits and hours while it serves the page itself; the row then says `agency_phone_status: refused` instead of a silent empty column; after three refusals in a row on one exit the run tries a fresh exit for the phone, twice a run at most.

**Removed listings, with idealista's date.** A watched listing URL comes back `ok` with every detail field, or `removed` with the day idealista says the advertiser took it down. Gone mode (`removedSince`, `removedUntil`) answers an area with the listings it lost in the window, each with its removal date, the last asking price and the days it was listed — built from the corpus's sweeps of the area (the first sweep of an area starts its list).

**Three markets, one row shape.** idealista.com (Spain), idealista.it (Italy) and idealista.pt (Portugal), each read from an exit of its own country; `market` on every row; the same columns whatever the host.

**Monitoring that does not repeat.** `onlyNew` with a saved task delivers only the listings no earlier run of the same memory delivered — a listing delivered before is skipped, never re-delivered or re-charged — and duplicates inside a run are dropped before they are charged. Put a search on a schedule and each run is the day's new listings.

**Nothing private, nothing to bring.** A private advertiser's name and phone are never read or stored — the row says `advertiser_type: private` and stops there; an agency's name, page, logo and phone are the business's public ones. No login, no cookie, no proxy field to fill: idealista is read through exits we maintain, at its own pace.

**Measured, not promised.** Every figure on this page comes from our own runs: a search page is about 46 KB on the wire for 30 listings, a listing's page about 33 KB, a removed listing's page 8 KB; the browser load that opens an exit is the one expensive step, and it runs once per exit, not once per page.

### All 74 fields of a row

Every row carries all of them. A field the source did not show arrives as null, never as a zero or an empty string, and the note says when that happens.

| field | type | what it holds |
|---|---|---|
| `row_type` | string | Always `listing` (one row shape for every mode). |
| `input` | string | The value you gave that this row answers: the search URL, the place name, or the listing URL or id. |
| `input_index` | integer | The 1-based position of the input in its list (0 for `location`). |
| `status` | string | `ok` = a live listing; `removed` = a listing idealista no longer shows (with its removal date); `not_found`, `invalid`, `failed` = a free row that says why there is no listing. |
| `status_detail` | string | Why a row is not `ok`, in words (for a `removed` row: idealista's own line). |
| `scraped_at` | string | When this run built the row (UTC). |
| `read_from` | string | Which path answered: `corpus` (our stored copy of the area, its last complete sweep younger than maxAgeHours) or `live` (idealista read during this run). |
| `read_at` | string | When the listing was read from idealista: this run (live) or the sweep that stored it (corpus). |
| `id` | string | idealista's listing id (the number in the listing's URL). |
| `url` | string | The listing's page on its host. |
| `market` | string | `es` (idealista.com), `it` (idealista.it) or `pt` (idealista.pt). |
| `operation` | string | `rent` or `sale`, from the search URL's path. |
| `property_type` | string | The type from the search URL's path: `homes`, `rooms`, `offices`, `garages`, `land`, `premises`, `buildings`, `storerooms`, `newDevelopments`; null when the path names none. |
| `title` | string | The listing's title as idealista writes it (the type and the street or area). |
| `location_text` | string | The neighbourhood and city line of the listing's page (with includeDetails or on a watched listing). |
| `price` | integer | The asking price in euros: per month for a rental, the total for a sale. |
| `currency` | string | Always `EUR`. |
| `price_unit` | string | `month` for a rental, `total` for a sale. |
| `price_before_drop` | integer | idealista's own former price when the card shows a price drop (the struck-out price), else null. |
| `price_drop_pct` | integer | idealista's own drop in percent next to that former price (`32` for −32 %), else null. |
| `size_m2` | integer | Built area in m² as the card shows it. |
| `rooms` | integer | Bedrooms (idealista's `hab.` / `locali` / `T` count). |
| `bathrooms` | integer | Bathrooms, from the listing's page (details). |
| `floor` | integer | The floor number (0 = ground floor); null when the card names none. |
| `floor_text` | string | The card's floor line as written (`1ª planta exterior sin ascensor`). |
| `is_exterior` | boolean | Exterior (street-facing) or interior, when the card says. |
| `has_lift` | boolean | A lift in the building, when the card or the page says. |
| `tags` | array | The card's badges (`Garaje incluido`, `Alquiler de temporada`, `Apartamento`). |
| `published_ago` | string | How long before the read idealista published or updated the listing, as the card shows it on the newest-first sort (`2 minutos`, `3 horas`). |
| `description_snippet` | string | The first lines of the description as the card shows them. |
| `thumbnail_url` | string | The card's first photo (an unsigned idealista CDN URL). |
| `photos_count` | integer | How many photos the listing has (the card's counter, or the page's gallery with details). |
| `advertiser_type` | string | `agency` (a professional) or `private` (an owner). A private advertiser's name and phone are never read or stored. |
| `agency_name` | string | The agency's name (agency listings only). |
| `agency_url` | string | The agency's page on idealista (agency listings only). |
| `agency_logo_url` | string | The agency's logo (agency listings only). |
| `agency_phone` | string | The agency's phone as idealista's own contact call returns it (details or a watched listing; agency listings only, never a private person's). Null when idealista refused the call on the run's exit: `agency_phone_status` says which. |
| `agency_phone_status` | string | With details: `served` (the number is on the row), `refused` (idealista refused its contact-phones call on this run's exit, a per-route refusal that varies by hour and exit; the listing's page still answered), `not_asked` (a private listing, or no phone button). Null without details. |
| `search_url` | string | The search this row came from. |
| `search_total` | integer | idealista's own count for the search at the time it was read (the page's heading). |
| `page` | integer | The search page the listing was on. |
| `rank` | integer | The listing's position in idealista's list for the search (1 = first). |
| `first_seen_at` | string | When a run or sweep first saw this listing (the corpus; this run for a listing never seen before). |
| `last_seen_at` | string | When a run or sweep last saw the listing live. |
| `days_listed` | integer | Days between first seen and last seen (or the day it was found removed). |
| `price_history` | array | Every price our reads saw, with the date: `[{at, price}, …]`, oldest first. |
| `price_previous` | integer | The price before the last change, when the corpus saw one. |
| `price_change` | integer | The last change in euros (negative = a cut), when the corpus saw one. |
| `price_changes_count` | integer | How many price changes the corpus saw. |
| `price_changed_at` | string | When the last price change was seen. |
| `removed_on` | string | idealista's own removal date (the listing's page says the advertiser removed it on this day); null on a live row, and on a removed listing whose page carries no date. |
| `removed_detected_at` | string | When a sweep or a watch found the listing gone. |
| `removed_text` | string | idealista's own line on the removal page, as written. |
| `description` | string | The full description from the listing's page (details). |
| `description_translated` | string | The description in `descriptionLanguage`, as idealista's own language switch on the listing's page serves it (an automatic translation); null when not asked. |
| `description_language` | string | The language of `description_translated` (`en`, `de`, …). |
| `features` | array | The page's feature lists as strings (basic features, the building, equipment). |
| `is_furnished` | boolean | Furnished, when the page's features say. |
| `has_terrace` | boolean | A terrace or balcony, when the page's features say. |
| `has_parking` | boolean | A parking space, when the page's features say. |
| `has_air_conditioning` | boolean | Air conditioning, when the page's features say. |
| `has_pool` | boolean | A pool, when the page's features say. |
| `energy_consumption` | string | The energy certificate's consumption letter (A–G; a class number on idealista.it), when the page shows one. |
| `energy_emissions` | string | The energy certificate's emissions letter, when the page shows one. |
| `photos` | array | Every photo of the page's gallery: `[{url, tag}, …]` — the full-size unsigned CDN URL and idealista's room tag (`Cocina`, `Giardino`, `Vistas`). |
| `plans` | array | The floor plans' image URLs, when the page has them. |
| `latitude` | number | The map's latitude from the listing's page. |
| `longitude` | number | The map's longitude from the listing's page. |
| `reference` | string | The advertiser's reference as the page shows it. |
| `updated_on` | string | The day idealista says the listing was last updated (the page's own line; the year inferred from the read). |
| `location_id` | string | idealista's location id of the listing from its page (`0-EU-ES-08-13-001-019-05-005-XX-XX`: the neighbourhood where the page names it, else the province), with details or on a watched listing. |
| `detail_read_from` | string | Where the detail fields came from: `live` (the page read this run, the `detail` event) or `corpus`; null when no details were asked. |
| `detail_read_at` | string | When the detail fields were read from idealista. |
| `is_offmarket` | boolean | idealista marks the listing off-market. |

### Ways to run it

**Search by URL.** Open idealista, set the place, the operation, the filters and the sort you want, and paste the page's address into `searchUrls` (several, one per line). Every page is read up to `maxItems`; `maxItems: 0` reads the whole search. The .it and .pt hosts work the same way.

**Search by place name.** Set `location` (`Barcelona`, `Sitges`, `08001`, `Milano`, `Lisboa`), `country`, `operation` and `propertyType`. idealista's own suggestions resolve the name to its first match and the run logs the search URL it took; for another type of property (offices, garages, land, new developments) paste the search URL instead.

**Add the listing's page.** Turn on `includeDetails`: every search row gets the description, every photo with its tag, the floor plans, the feature lists and flags, bathrooms, the energy letters, the coordinates, the reference, `updated_on` and, for an agency listing, `agency_phone`. One page per listing; a watched listing always has them.

**Watch listings.** Put listing pages or bare ids into `listingUrls` (with `country` for a bare id). A live listing comes back with every detail field; a removed one with `status: removed`, `removed_on` and idealista's own line; an id idealista has no page for is a free `not_found` row.

**The listings an area lost.** Give the area's search URL and `removedSince` (and `removedUntil`): the rows are the listings removed in the window, each with `removed_on`, the last price, `first_seen_at` and `days_listed`. The list is built by the sweeps of the area — a run that walks the whole area (`maxItems: 0`, or any gone-mode run) is a sweep; the first sweep of an area starts its list.

**Fresh or live.** `maxAgeHours` (24 by default) decides when a search is answered from the corpus: an area swept whole within that many hours answers at once with `read_from: corpus` and `read_at` the sweep's time; otherwise the run reads idealista live and updates the corpus. `0` reads live every time.

**Only new, on a schedule.** Save the run as a task, turn on `onlyNew` (and name the memory with `stateStoreName` if you like), schedule it: each run delivers only the listings no earlier run of that memory delivered.

### Everything you can ask for

#### What to read

Give search URLs, a place name, or listing URLs — any of them, or several at once.

| input | accepts | default | what it changes |
|---|---|---|---|
| `searchUrls` | `array` | — | idealista search pages, one per line, on idealista.com, idealista.it or idealista.pt: the place, the operation, the property type, the filters and the sort as idealista's own search page shows them (e.g. `https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc`). Every page of the search is read up to `maxItems`. A listing link in this list is read as a watched listing. |
| `location` | `string` | — | A place as you would type it into idealista's search box (`Barcelona`, `Sitges`, `08001`, `Milano`, `Lisboa`), resolved by idealista's own suggestions to its first match; with `country`, `operation` and `propertyType` below. Empty when you give search URLs. |
| `country` | `es` / `it` / `pt` | `"es"` | The market of `location`, and of a bare listing id in `listingUrls`. A search or listing URL carries its own host. |
| `operation` | `rent` / `sale` | `"rent"` | With `location`: rent or sale. A search URL carries its own operation. |
| `propertyType` | `homes` / `rooms` | `"homes"` | With `location`: homes or rooms. Every other type idealista lists (offices, garages, land, premises, new developments) is read from its search URL. |

#### Watch listings

| input | accepts | default | what it changes |
|---|---|---|---|
| `listingUrls` | `array` | — | Listing pages (`https://www.idealista.com/inmueble/112816892/`, the .it `/immobile/` and .pt `/imovel/` forms) or bare ids (read on `country`'s host), one per line. Each is read live: a live listing comes back with every detail field; a removed one comes back with status `removed` and the date idealista shows. |

#### Removed listings

| input | accepts | default | what it changes |
|---|---|---|---|
| `removedSince` | `string` | — | Gone mode: with search URLs, return the listings of those areas that went away on or after this day (YYYY-MM-DD), each with idealista's own removal date, the last asking price and the days it was listed — instead of the live listings. The list comes from the corpus's sweeps of the area: an area never swept before has nothing to report yet. |
| `removedUntil` | `string` | — | Gone mode: the end of the window (YYYY-MM-DD, inclusive). Empty = today. |

#### What a row carries

| input | accepts | default | what it changes |
|---|---|---|---|
| `includeDetails` | `boolean` | `false` | Add the detail page's fields to every search row: the full description, every photo with its room tag, the floor plans, the feature lists, bathrooms, furnished / terrace / parking / air conditioning / pool flags, the energy letters, the coordinates, the reference, the day idealista last updated the listing, and the agency's phone (agency listings only). One extra page per listing; a watched listing always has them. |
| `descriptionLanguage` | `string` | — | With includeDetails (and on watched listings): add the description translated into this language, as idealista's own language switch on the listing page serves it (`description_translated`, `description_language`). Empty = the original text only. |
| `maxAgeHours` | `integer` | `24` | A search is answered from the corpus (our stored copy of the area, shared by every run) when the area's last complete sweep is younger than this many hours; otherwise idealista is read live and the corpus updated. 0 = always read live. Every row says which path answered (`read_from`) and when the listing was read (`read_at`). |
| `maxItems` | `integer` | `100` | Stop after this many charged rows across every search and watched listing (free rows do not count). 0 = no cap: the whole search to its last page. |

#### Monitoring

| input | accepts | default | what it changes |
|---|---|---|---|
| `onlyNew` | `boolean` | `false` | With a saved task on a schedule: deliver only the listings no earlier run of the same memory delivered (a listing delivered before is skipped, never re-delivered or re-charged). The memory is named by `stateStoreName`, or derived from the search URLs. |
| `stateStoreName` | `string` | — | A name for the memory `onlyNew` uses, so the same saved task finds it run after run. Optional: without it the name is derived from the search URLs. |

#### Advanced

| input | accepts | default | what it changes |
|---|---|---|---|
| `proxyUrl` | `string` | — | Optional. Your own residential proxy as `scheme://user:pass@host:port`, used in front of the Actor's own exits. It must pin one IP for the run (a sticky session) and be in the market's country: idealista's cookie is bound to the exit that minted it. |

### If an input can't be used

The run still ends Succeeded and charges nothing. Its status message begins `INPUT REJECTED` and says what to change. The dataset holds a single row, `{"error": true, "code": …, "message": …}`, with no results, and the key-value record `ERROR` repeats it, so an agent should check `error` on the first row. A search that finds nothing is not an error, only an empty dataset. A run marked Failed is a fault on our side, not your input; it costs nothing and reaches us.

### Before you run it

**Which path answered my search, and how old is the data?** Every row says: `read_from` is `corpus` or `live`, `read_at` is when the listing was read from idealista. A corpus row is never older than `maxAgeHours` (24 by default). A live row is the page of the minute.

**Does it need a login, cookies or a proxy?** No. idealista is read logged out through exits we maintain, each opened by one browser load and then read at idealista's own pace. There is no cookie or proxy field because there is nothing for you to supply; `proxyUrl` is optional and must be a sticky residential exit in the market's country.

**How complete is a search?** The walk follows idealista's own next link to the last page; `search_total` on every row is idealista's own count of the hour, and the run summary says COMPLETE when the last page was reached, TRUNCATED when `maxItems` stopped it, PARTIAL when the run's time limit did. The count and the list can differ by a few listings that enter or leave while the pages are read.

**What is not served logged out?** A private advertiser's name and phone (never read: the corpus keeps none), the per-listing statistics (views, contacts: idealista's page opens its login for them), and the mobile app's location ids as an input (a place name or the search URL stands in).

**Why did a run read live when I expected the corpus?** The corpus answers only an area whose last complete sweep is younger than `maxAgeHours`; a search nobody swept whole in that window — a new area, new filters, a different sort — is read live and becomes the next sweep. A capped run (`maxItems` below the area's count) updates the listings it read but does not make the area fresh.

**What does a removed listing's date mean?** `removed_on` is the day idealista itself prints on the listing's page after the advertiser took it down ("El anunciante lo dio de baja el …", "L'inserzionista lo ha disattivato …", "O anúncio foi desativado no dia …"); a few removal pages carry no date, and then the row says so and `removed_detected_at` is the day a sweep found it gone.

**Is this legal, and what about personal data?** Everything here is read from what idealista serves anyone, logged out: no account, no cookie, nothing behind a login, paced under the site's own rhythm. What is kept is what a property listing is — the home, its price, its history, the agency behind it. A private advertiser's name and phone are never read or stored, so no row and no corpus record carries them. What you may do with a public listing depends on your purpose and your jurisdiction, and that part is yours, not ours.

**How do I get the rows out?** They land in the run's dataset: download them as JSON, CSV, Excel or XML from the run page, or read the same rows over the Apify API (the API tab has the call for this Actor). Save a run as a task, schedule it, and point the store's integrations at it — webhooks, Zapier, Make, Slack, Google Drive — or let an agent call the Actor over MCP. Every row carries a stable `id` and `url`, so repeated runs deduplicate against your own table.

**Who makes this, and what else is there?** [Moxlade](https://moxlade.com) — corpora your agent can ask. Its other actors are the [Vinted sellers database](https://apify.com/moxlade/vinted-users), the [Upwork freelancer census](https://apify.com/moxlade/upwork-freelancers), the [Instagram email scraper](https://apify.com/moxlade/instagram-email-scraper) and the [Reddit scraper](https://apify.com/moxlade/reddit-scraper).

### What it costs

**Pay per row you get.** No start fee, no minimum, no platform usage billed to you, and an empty run is free. Prices fall with your Apify plan.

| per 1,000 | Free | Starter | Scale | Business |
|---|---|---|---|---|
| Listing row (from the corpus) (`listing`) | $1.00 | $0.85 | $0.70 | $0.50 |
| Listing row (read from idealista now) (`listing_live`) | $1.00 | $0.85 | $0.70 | $0.50 |
| Detail page read (`detail`) | $2.33 | $2.33 | $2.33 | $2.33 |
| Removed listing row (`removed`) | $1.00 | $0.85 | $0.70 | $0.50 |

A listing row is **$0.001** on the Free plan ($0.0005 on Business), the same whether the corpus answered it (`listing`) or idealista was read during your run (`listing_live`); `read_from` on the row says which. A listing's own page read during your run (`includeDetails`, or a watched listing that is live) adds **$0.00233** on every plan: it is the slow part of a run (about two seconds a page on idealista's side, and idealista's phone call for an agency listing), priced at what it costs us. A listing idealista no longer shows (a watched listing that is gone, a gone-mode row) is **$0.001** ($0.0005 on Business). The history columns on every row — first seen, days listed, the price history, the removal date — cost nothing extra. Free: a search or a listing idealista answers nothing for, a value that names nothing, a listing the exits could not read, a listing an earlier run of the same memory already delivered (`onlyNew`), and a run that failed before its first row. A run that stops early (a timeout, an abort) is charged for the rows it delivered. `maxItems` counts the charged rows (a detail rides on its row), so it is your spending cap; before the first request the log says the most the rows can cost at your plan's prices. With `maxItems: 0` the run reads each search to idealista's last page: about as many rows as idealista's own count on the first page (a Barcelona rental search under €1,300 held 248 listings on 2026-10-15, read whole in 52 seconds). Enterprise plans have their own rates; the Actor's Pricing tab shows the price for your plan.

| event | what one event is | per 1,000 |
|---|---|---|
| `listing` | One search row answered from the corpus: the area's last complete sweep is younger than maxAgeHours. The card fields, the position in idealista's list, and the history: first seen, last seen, days listed, the price history. Charged only once the row is in your dataset. | $1.00 |
| `listing_live` | One search row read from idealista during your run (the corpus was stale or you asked for a live read), or a watched listing that is live. The same fields, read this minute. | $1.00 |
| `detail` | The listing's own page read during your run (includeDetails, or a watched listing): the full description, every photo with its tag, the floor plans, the feature lists, the energy letters, the coordinates, the reference, the day idealista last updated it, the agency's phone. One per row whose page was read live; a row whose details came from the corpus owes none. | $2.33 |
| `removed` | One row of a listing idealista no longer shows: a watched listing that is gone, or a listing of an area removed in your window (gone mode), with idealista's own removal date, the last asking price and the days it was listed. | $1.00 |

### Who asks this

**Home hunters and relocation teams** — the newest listings of an area every morning with `onlyNew`, each with how long it has been listed and whether its price moved — and the ones that went away, with idealista's date.

**Agencies and investors** — an area's whole list with idealista's own count, the price history of every listing from before your first run, the days on market, and the listings removed in a window — the signal a single page cannot give.

**Analysts and data pipelines** — one call, JSON rows with a stable `id`, `url`, `market` and dated columns, from three markets with one row shape; the corpus answers a tracked area without a browser.

### Questions

support@moxlade.com

*Every figure on this page was measured on our own runs or read from our own tables, on the date given next to it.*

# Actor input Schema

## `searchUrls` (type: `array`):

idealista search pages, one per line, on idealista.com, idealista.it or idealista.pt: the place, the operation, the property type, the filters and the sort as idealista's own search page shows them (e.g. `https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc`). Every page of the search is read up to `maxItems`. A listing link in this list is read as a watched listing.

## `location` (type: `string`):

A place as you would type it into idealista's search box (`Barcelona`, `Sitges`, `08001`, `Milano`, `Lisboa`), resolved by idealista's own suggestions to its first match; with `country`, `operation` and `propertyType` below. Empty when you give search URLs.

## `country` (type: `string`):

The market of `location`, and of a bare listing id in `listingUrls`. A search or listing URL carries its own host.

## `operation` (type: `string`):

With `location`: rent or sale. A search URL carries its own operation.

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

With `location`: homes or rooms. Every other type idealista lists (offices, garages, land, premises, new developments) is read from its search URL.

## `listingUrls` (type: `array`):

Listing pages (`https://www.idealista.com/inmueble/112816892/`, the .it `/immobile/` and .pt `/imovel/` forms) or bare ids (read on `country`'s host), one per line. Each is read live: a live listing comes back with every detail field; a removed one comes back with status `removed` and the date idealista shows.

## `removedSince` (type: `string`):

Gone mode: with search URLs, return the listings of those areas that went away on or after this day (YYYY-MM-DD), each with idealista's own removal date, the last asking price and the days it was listed — instead of the live listings. The list comes from the corpus's sweeps of the area: an area never swept before has nothing to report yet.

## `removedUntil` (type: `string`):

Gone mode: the end of the window (YYYY-MM-DD, inclusive). Empty = today.

## `includeDetails` (type: `boolean`):

Add the detail page's fields to every search row: the full description, every photo with its room tag, the floor plans, the feature lists, bathrooms, furnished / terrace / parking / air conditioning / pool flags, the energy letters, the coordinates, the reference, the day idealista last updated the listing, and the agency's phone (agency listings only). One extra page per listing; a watched listing always has them.

## `descriptionLanguage` (type: `string`):

With includeDetails (and on watched listings): add the description translated into this language, as idealista's own language switch on the listing page serves it (`description_translated`, `description_language`). Empty = the original text only.

## `maxAgeHours` (type: `integer`):

A search is answered from the corpus (our stored copy of the area, shared by every run) when the area's last complete sweep is younger than this many hours; otherwise idealista is read live and the corpus updated. 0 = always read live. Every row says which path answered (`read_from`) and when the listing was read (`read_at`).

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

Stop after this many charged rows across every search and watched listing (free rows do not count). 0 = no cap: the whole search to its last page.

## `onlyNew` (type: `boolean`):

With a saved task on a schedule: deliver only the listings no earlier run of the same memory delivered (a listing delivered before is skipped, never re-delivered or re-charged). The memory is named by `stateStoreName`, or derived from the search URLs.

## `stateStoreName` (type: `string`):

A name for the memory `onlyNew` uses, so the same saved task finds it run after run. Optional: without it the name is derived from the search URLs.

## `proxyUrl` (type: `string`):

Optional. Your own residential proxy as `scheme://user:pass@host:port`, used in front of the Actor's own exits. It must pin one IP for the run (a sticky session) and be in the market's country: idealista's cookie is bound to the exit that minted it.

## Actor input object example

```json
{
  "searchUrls": [
    "https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc"
  ],
  "country": "es",
  "operation": "rent",
  "propertyType": "homes",
  "includeDetails": false,
  "maxAgeHours": 24,
  "maxItems": 100,
  "onlyNew": false
}
```

# Actor output Schema

## `results` (type: `string`):

This run's rows, read from its default dataset. One row per listing (`status` says whether it is live or removed; a free row says why there is none):

## `runSummary` (type: `string`):

How the run went: rows by event and by path (corpus / live), free rows by status, one report per search URL (idealista's count, pages read, the verdict COMPLETE / TRUNCATED / PARTIAL, the gone checks) and per watched listing, the browser loads and calls, the corpus hits and writes, and what idealista does not serve logged out.

# 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 = {
    "searchUrls": [
        "https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("moxlade/idealista-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 = { "searchUrls": ["https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc"] }

# Run the Actor and wait for it to finish
run = client.actor("moxlade/idealista-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 '{
  "searchUrls": [
    "https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/?ordenado-por=fecha-publicacion-desc"
  ]
}' |
apify call moxlade/idealista-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,moxlade/idealista-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/Zl2NQyNV79MDnRWjm/builds/YNQzpOQKb1mGxf9ZP/openapi.json
