# pisos.com Scraper - Spain Real Estate, Properties & Rentals (`plum_spear/aztec-pisos`) Actor

Scrape pisos.com property listings across Spain for sale and rent. Extract property prices in EUR, address, city, region, coordinates, images, and listing URLs as clean structured data.

- **URL**: https://apify.com/plum\_spear/aztec-pisos.md
- **Developed by:** [Roberto Kerber](https://apify.com/plum_spear) (community)
- **Categories:** Real estate, Lead generation, E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#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

## pisos.com Scraper - Spain Real Estate Listings, Property Prices & Rentals

> Built and maintained by **Az Digital Consulting LU**.

**Scrape pisos.com at scale.** This scraper turns [pisos.com](https://www.pisos.com/) - one of Spain's largest real-estate portals - into a clean, structured data feed. Choose **sale or rent** and a location, and get flat JSON for every property: **title, price in EUR, city, region, country, latitude, longitude, image** and a direct URL to the listing.

No proxy setup, no HTML wrangling, no account. Run it once for a market snapshot, or **schedule it daily** to track how asking prices and supply move over time.

***

### What does pisos.com Scraper do?

You pick an operation - `venta` (for sale) or `alquiler` (for rent) - and a Spanish location such as `madrid`, `barcelona`, `valencia` or `sevilla`. It returns every matching property as clean, flat JSON.

Prices arrive as **numbers** (`1600000`, not `"1.600.000 €"`), so you can sort, filter and run calculations the moment the run finishes - and the original string is kept in `priceRaw`, so a rental is unambiguous (`"2.600 €/mes"`). Every listing also carries **latitude and longitude**, so results can go straight onto a map or into a geospatial join.

It is the fastest way to **scrape pisos.com listings** for any Spanish market - whether you are tracking asking prices, sizing supply in a city, doing real-estate market research, or building a price index.

***

### Why use pisos.com Scraper?

- **Price monitoring** - follow asking prices for property across any Spanish city.
- **Rental market analysis** - pull `alquiler` listings with monthly rent as a number.
- **Market research** - measure supply by city, region and price band.
- **Geospatial analysis** - every listing ships coordinates, ready to map.
- **Price indexes** - schedule daily runs and build a real price history.

***

### How to use it

1. Click **Try for free**.
2. Choose the **operation**: `venta` (for sale) or `alquiler` (for rent).
3. Type a **location** (`madrid`, `barcelona`, `valencia`, `sevilla`, `malaga`...).
4. Optionally set a price range.
5. Set **Max items**, click **Start**, and download the results as JSON, CSV, Excel or HTML.

***

### Input

| Field | Description |
|---|---|
| `operation` | `venta` (for sale) or `alquiler` (for rent) |
| `location` | Spanish city/province slug, e.g. `madrid`, `barcelona`, `valencia` |
| `priceMin` / `priceMax` | Price range in EUR (monthly rent for `alquiler`). Optional |
| `maxItems` | How many listings to collect |

```json
{
  "operation": "venta",
  "location": "madrid",
  "priceMin": 100000,
  "priceMax": 500000,
  "maxItems": 100
}
```

***

### Output

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

Real item from a real run (`operation=venta`, `location=madrid`, 2026-09-05):

```json
{
  "id": "65098413759.100500",
  "title": "Piso en calle de la Princesa, 76",
  "price": 1600000,
  "priceRaw": "1.600.000 €",
  "currency": "EUR",
  "url": "https://www.pisos.com/comprar/piso-gaztambide-65098413759_100500/",
  "image": "https://fotos.imghs.net/mm-wp/1005/101/1005_2935432214101_1_202606051854533172743.jpg",
  "city": "Madrid Capital",
  "region": "Madrid",
  "country": "ES",
  "latitude": 40.4325,
  "longitude": -3.71735,
  "operation": "venta",
  "scrapedAt": "2026-09-05T13:30:46Z"
}
```

And a rental (`operation=alquiler`, `location=barcelona`) - note `priceRaw` keeps the `/mes` suffix while `price` is the plain monthly number:

```json
{
  "id": "65857798720.437300",
  "title": "Casa en Passeig de les Camèlies, 17",
  "price": 2600,
  "priceRaw": "2.600 €/mes",
  "currency": "EUR",
  "url": "https://www.pisos.com/alquilar/casa-sant_cugat_del_valles_mira_sol08197-65857798720_437300/",
  "image": "https://fotos.imghs.net/mm-wp/4373/65857798720.437300/4373_65857798720_33_20260709151242271.jpg",
  "city": "Sant Cugat del Vallès",
  "region": "Barcelona",
  "country": "ES",
  "latitude": 41.464066,
  "longitude": 2.052408,
  "operation": "alquiler",
  "scrapedAt": "2026-09-05T13:31:24Z"
}
```

#### Data fields

| Field | Type | Description |
|---|---|---|
| `id` | string | pisos.com listing ID |
| `title` | string | Listing headline (property type and street) |
| `price` | number | Price in EUR. Monthly rent when `operation=alquiler`. `null` when the ad shows no price |
| `priceRaw` | string | Price exactly as shown, e.g. `1.600.000 €` or `2.600 €/mes` |
| `currency` | string | Always `EUR` |
| `url` | string | Direct link to the listing |
| `image` | string | Main photo URL |
| `city` | string | Municipality, e.g. `Madrid Capital` |
| `region` | string | Province, e.g. `Madrid` |
| `country` | string | Country code, e.g. `ES` |
| `latitude` / `longitude` | number | Coordinates of the property |
| `operation` | string | `venta` or `alquiler` |
| `scrapedAt` | string | Collection timestamp (ISO 8601, UTC) |

***

### Pricing

**Pay per event: per 1,000 listings returned, plus a minimal actor-start event.**

No subscription, no monthly minimums - you only pay for the data you actually extract. New Apify users get free monthly platform credits to test at no cost before scaling.

***

### Tips

- Narrower locations run faster and cost less. A city plus a price range is the sweet spot.
- A search caps out at roughly 100 pages (~3,000 listings). To collect a whole province, run it per city rather than raising `maxItems`.
- Schedule a daily run on the same location to build a price history you can chart.
- `priceRaw` is the safest field for auditing: it is the exact string the portal displayed.

***

### FAQ

**Is scraping pisos.com legal?**
This Actor collects only **publicly available listing data** - the same information any visitor sees without logging in. It deliberately collects **no personal data**: advertiser and agency names, phone numbers and emails are never returned, even though some appear on the page. You remain responsible for how you use the data; review the target site's terms and applicable law (including GDPR) for your use case.

**Do I need a proxy?**
The Actor reads the portal's public listing pages directly. If you see HTTP 403 in the log, enable an Apify residential proxy in the Actor's run settings.

**Why is `price` sometimes null?**
Some ads are published without a price ("a consultar"). The field is left `null` on purpose - never a fake `0` - so averages and filters stay correct.

**Why did I get fewer listings than I asked for?**
The location simply had fewer listings, your price filter excluded the rest, or the search hit the portal's page cap. Try a broader location or a wider price range.

**Can I get another Spanish or European property portal?**
Yes - open an issue on the **Issues** tab.

***

### Support

Found a bug or need a field that is not here? Open an issue on the **Issues** tab of this Actor. Issues are answered.

***

### Notes for maintainers (Az Digital Consulting LU)

Source validated on **2026-09-05**. Everything below is measured, not assumed.

- List URL: `https://www.pisos.com/{operation}/pisos-{location}/`; pagination is a `/N/` path suffix. The `pisos-` prefix is mandatory - `/venta/madrid/` answers **404**.
- 30 listings per page. Each card ships its own `ld+json` block with `@type=SingleFamilyResidence`.
- **The price is NOT in the ld+json.** It lives in the card markup under `class="ad-preview__price"`.
- **Alignment rule (this is the bug the parser exists to prevent, cf. `aztec-standvirtual`):** the HTML is split per card with `re.split(r'(?=<div id="[\d.]+" class="ad-preview)')`, and the `<div id>` is matched against the ld+json `@id` **inside that same slice**. Pairing the ld+json list against a price list **by index** silently mismatches prices to properties.
  - Verified by opening the detail page of 4 listings spread across page 1 (cards 1, 8, 16, 30): the listed price matched the detail-page price in **4/4**.
- **End of pagination is a REDIRECT, not an empty page.** Past the cap the portal answers HTTP 200 and redirects to page 1, re-serving the same 30 cards. Measured: pages 2/5/20/50/100 stay put; pages 200/400 redirect. Stopping on "empty page" would loop forever - `_fetch` returns `None` when the final URL no longer contains `/N/`.

#### Local runs actually executed (2026-09-05)

| Run | Input | Result |
|---|---|---|
| venta | `madrid`, `maxItems=65` | 65 items, 65 unique ids, **65/65 with price**, 0 with `price==0` |
| alquiler | `barcelona`, `maxItems=40`, `priceMin=900`, `priceMax=3000` | 40 items, all within range (min 1.029, max 3.000 EUR), `€/mes` parsed |
| alquiler | `soria`, `maxItems=5000` | 10 items - pagination ended on its own, no loop |

#### ⚠️ PENDING: datacenter IP test

All of the above ran from the **residential** egress IP `213.135.246.95` (K11), confirmed with `curl -s https://api.ipify.org`. **Apify runs from datacenter IPs.** Whether pisos.com answers 200 or 403 from a datacenter IP is **NOT yet proven**. Before publishing:

1. Push a build and run it on the Apify platform.
2. If the log shows HTTP 403, either enable Apify residential proxy in the run settings, or add a `/pisos` endpoint to `scraper-k11/app.py` and route through the K11 cluster (same pattern as `aztec-olx` / `aztec-webmotors`).

# Actor input Schema

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

Whether to scrape properties for sale (venta) or for rent (alquiler).

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

Spanish city, province or region slug as pisos.com writes it, e.g. 'madrid', 'barcelona', 'valencia', 'sevilla', 'malaga', 'alicante'. The 'pisos-' prefix is added automatically.

## `priceMin` (type: `integer`):

Only return listings at or above this price. For rentals the value is the monthly rent. Optional.

## `priceMax` (type: `integer`):

Only return listings at or below this price. For rentals the value is the monthly rent. Optional.

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

Maximum number of property listings to scrape. The portal serves 30 listings per page and caps a search at roughly 100 pages (~3,000 listings) - narrow by location to collect more.

## Actor input object example

```json
{
  "operation": "venta",
  "location": "madrid",
  "maxItems": 100
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# 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 = {
    "location": "madrid"
};

// Run the Actor and wait for it to finish
const run = await client.actor("plum_spear/aztec-pisos").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 = { "location": "madrid" }

# Run the Actor and wait for it to finish
run = client.actor("plum_spear/aztec-pisos").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 '{
  "location": "madrid"
}' |
apify call plum_spear/aztec-pisos --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plum_spear/aztec-pisos"
        }
    }
}

```

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/cWImhFN6B0q0ALIZN/builds/DfGaHzdF5gtxxNIH0/openapi.json
