Fotocasa Spain Scraper - Properties, Prices & Agents
Pricing
from $1.00 / 1,000 listing scrapeds
Fotocasa Spain Scraper - Properties, Prices & Agents
Scrape Fotocasa.es property listings for sale and rent across Spain. Extract 140+ fields including prices, floor areas, bedrooms, bathrooms, addresses, GPS coordinates, agencies, phone numbers, energy ratings and photos. Covers all regions and property types.
Pricing
from $1.00 / 1,000 listing scrapeds
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
4
Total users
0
Monthly active users
3 days ago
Last modified
Categories
Security
Limited permissionsShare
Fotocasa.es Scraper: Property Listings, Prices & Agency Contacts
Fotocasa.es Scraper turns fotocasa.es into a clean property-data feed for Spain. Get price, surface, rooms, bathrooms, full address, GPS coordinates, agency name and phone, and 140+ fields per listing, for both buy and rent, every region and every property type. Search by location and filters, or paste direct fotocasa.es search-result URLs, switch on per-listing detail enrichment for energy rating and agent contact, then export to JSON, CSV or Excel, or pull results straight into your app through the API.
Why This Scraper?
- 140+ fields per listing. Every record carries both camelCase and snake_case field names, so the output drops into any existing pipeline without remapping.
- Buy and rent, every region, every property type. Flats, houses, duplexes, penthouses, studios, lofts, garages, offices, warehouses, land and more.
- Rich location data. City, neighborhood and precise latitude/longitude on every listing, straight from the search results.
- Agency and contact details on every listing, plus optional agent name, email and listing reference when detail enrichment is on.
- Two input modes. Search by location with filters, or feed direct fotocasa.es search-result URLs; either way you choose whether to fetch full listing details.
- Built for monitoring. Incremental mode tracks one saved search across scheduled runs and classifies each listing as new, updated or unchanged by default (reappeared needs Emit expired listings), without you pasting any run id.
- Fails loudly, not silently. A run where every request was refused stops with a clear error instead of billing an empty result as if the search itself came back with nothing.
Use Cases
- Real estate investors: track price per square metre across neighborhoods to spot undervalued listings.
- Proptech platforms and aggregators: build a structured buy-and-rent property feed for Spain to power search or comparison tools.
- Relocation and expat services: monitor new listings in target cities so clients hear about matches quickly.
- Market researchers and appraisers: build historical price and surface datasets for a city or region.
- Agencies and portals: track how competitor listings price and how long they stay on the market, using Incremental mode's change history.
Data You Get
Sample shape: values are illustrative placeholders, not from a live listing.
| Field | Example |
|---|---|
id | "1_000000000" |
propertyId | "000000000" |
title | "Calle de Ejemplo, Centro" |
rawPrice | 295000 |
currency | "EUR" |
pricePerSqm | 4538.46 |
surface | 65 |
rooms | 3 |
baths | 2 |
floor | "4" |
propertyType | 2 |
propertySubtype | 1 |
operation | "sale" |
city | "Madrid" |
neighborhood | "Centro" |
latitude | 40.4168 |
longitude | -3.7038 |
agencyName | "Example Agency SL" |
clientAlias | "Example Agency" |
clientType | "professional" |
phone | "+34000000000" |
images | ["https://static.fotocasa.es/images/ads/0000?rule=original"] |
photosCount | 12 |
url | "https://www.fotocasa.es/es/comprar/vivienda/madrid-capital/centro/000000000/d" |
Every listing also carries a description, a publicationDate (derived from the listing's creation timestamp, present on every row), a top-level zipCode, promotion/highlight flags (isNew, isNewConstruction, isVirtualTour, isTop, ...) and dozens of id/type fields (transactionTypeId, typeId, subtypeId, clientTypeId, ...) in both camelCase and snake_case. energyCertificate, reference, extraFeatures, constructionType, groundSurface, qualityRate, purchaseType, agentName, contactName and creationDate and contracts stay empty on every record unless fetchDetails is on, since they only come from the listing's own detail page; the same goes for the street name and number (address.street, fetchDetails only). Each row also carries isAuctioned, isRentedWithTenants, hasPhoneVisibility and antiquity (age bucket; the detail page value replaces the search-card one when fetchDetails is on), and the address object keeps the site's location hierarchy (district, cityZone, county, regionLevel1, regionLevel2, upperLevel). The legacy agentEmail key is kept for compatibility, but the site does not publish agent emails, so it is always empty. When Incremental mode is on, every returned record also carries changeType (NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED), changedFields, firstSeenAt and lastSeenAt.
How to Use
- Pick a mode:
search(location + filters) orurl(direct fotocasa.es search-result URLs). - For search mode, set Locations, Operation, Property Type, Sort By and any price/rooms/baths/surface filters. In search mode a typed place name such as Valencia is resolved on fotocasa.es (slugs such as
madrid-capitalare used as they are, with no lookup; a name the site does not know stops the run with an error, and if the lookup itself cannot be read the run fails with a connection message instead). For URL mode, paste fotocasa.es search pages, agency pages or single listing pages (ending in /d); filter fields are ignored in this mode. - Turn on Fetch full details if you need energy rating, full features, reference and agent/contact name and email from each listing's own page.
- Set Max items to control run size and cost, then click Start. Download the dataset as JSON, CSV or Excel, or read it through the API.
Search a city for homes to buy:
{"mode": "search","locations": ["madrid-capital"],"operation": "comprar","propertyType": "viviendas","maxListings": 50}
Rent search with filters and sorting:
{"mode": "search","locations": ["barcelona-capital"],"operation": "alquiler","minPrice": 800,"maxPrice": 1500,"minRooms": 2,"sortBy": "price_asc","maxListings": 100}
Direct search URLs with full detail enrichment:
{"mode": "url","urls": ["https://www.fotocasa.es/es/comprar/viviendas/valencia-capital/todas-las-zonas/l"],"fetchDetails": true,"maxListings": 200}
Daily monitoring of one search, with expiry tracking:
{"mode": "search","locations": ["madrid-capital"],"operation": "comprar","propertyType": "pisos","incrementalMode": true,"emitExpired": true,"maxListings": 0}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/fotocasa-es-scraper").call(run_input={"mode": "search","locations": ["madrid-capital"],"operation": "comprar","maxListings": 50,})for listing in client.dataset(run["defaultDatasetId"]).iterate_items():print(listing["title"], listing["rawPrice"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/fotocasa-es-scraper').call({mode: 'search',locations: ['madrid-capital'],operation: 'comprar',maxListings: 50,});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Full-catalogue runs and resuming
To walk an entire search (not just a page or two), set Max items to 0 and leave Max Pages Per Search empty (or 0): the actor then paginates until the site itself runs out of results. If a full-catalogue run is interrupted (platform migration, manual abort, or a fresh run days later to pick up new listings), pass the previous run's id as Resume from run ID: the actor pages through that run's dataset, seeds its dedup set with the listing ids already collected there, and then scrapes only new listings. An interrupted run also checkpoints its own seen-listing state during the run and resumes from it automatically on a platform migration; that checkpoint is cleared once a run finishes cleanly.
Incremental mode and REAPPEARED/EXPIRED
resumeFromRunId continues one specific interrupted run. Incremental mode is for the opposite case: you schedule this actor to run the same search again and again (daily, weekly) and want only what changed, without pasting any id. Turn it on and the actor keeps a baseline in its own key-value store, keyed on the search/URL and every filter and fetchDetails setting (raising or lowering Max items/Max Pages between runs never forks a new baseline). Each listing is then classified:
- NEW: not seen before.
- UPDATED: a tracked field changed;
changedFieldslists which ones. - UNCHANGED: nothing changed. Suppressed by default (not returned, not billed); turn on
emitUnchangedto get the full snapshot every run instead. - REAPPEARED: this only ever fires for a listing whose stored state was already marked not-present by an earlier run, which only happens once that earlier run had
emitExpiredon and completed a full, uncapped, non-resumed scan. It is never the outcome for an ordinary unchanged or first-seen listing. - EXPIRED: was tracked, no longer found. Only produced when
emitExpiredis on, and only once a run has proven a complete scan of the tracked search: amaxListings/maxPagescap, or aresumeFromRunIdrun, skips expiry detection entirely for that run rather than risk tombstoning a listing that simply wasn't reached yet.
What is compared. Price, reduced price, surface, rooms, bathrooms, location, title, description, features, availability flags and the agency/contact summary fields decide UPDATED, and changedFields names what moved. Photo and media URLs (multimedia, multimedias, images, imageUrl, previewImage, photoUrls), the per-session isVisited / isFaved flags (and snake_case twins), the listing-age object date and the day counter inside dateOriginal / date_original (diff and unit, which change every day while the creation timestamp stays fixed and is still compared), the derived zipCode and publicationDate twins and the raw agency profile object are returned as usual but never compared, so a rotating image host cannot make a listing UPDATED. Add your own with Ignore fields when detecting changes (ignoreFieldsForChanges, dotted paths such as address.city work; a derived top-level twin such as city must be listed too). Upgrading never creates a one-time wave of UPDATED rows: the stored baseline is re-read with the current ignore set. The auction, rented-with-tenants, phone-visibility, antiquity and contracts fields and the location hierarchy (address.district, address.cityZone, address.county, address.regionLevel1, address.regionLevel2, address.upperLevel) are compared too; a listing stored before these fields existed skips them once (so it reads UNCHANGED, not UPDATED), and from the next run on they decide UPDATED like any other field. With Fetch details on, a listing whose detail page did not load in one of two runs (or whose stored baseline came from such a run) is never reported as UPDATED because of it: the detail-page fields (energy certificate, extra features, contracts, reference, contact name and email, and so on) are compared only between two runs that both loaded the detail page, and the fields the detail page refines (antiquity, description, features, address and coordinates, neighbourhood, agency name, phone, photo count) are skipped only when exactly one of the two runs has detail data. Price fields always stay compared.
Price monitoring. changeScope = priceAndStock compares only price, reduced price, price per m2, price type and the occupied/discarded flags. compactUpdates slims UPDATED and EXPIRED rows to ids, url, title, current price and priceChanges (old and new); compact rows carry no detail-page data, so the detail enrichment event is not charged for them. expireAfterMissedScans waits for N consecutive complete scans before a listing is marked EXPIRED. EXPIRED means the listing was not found in the search results, not proof that it was delisted or sold. All three default to off, so existing output is unchanged. Expiry is never produced from a pasted page /l/N with N above 1, a run cut by the Max items cap, or a resumed run, and EXPIRED rows never exceed the remaining Max items.
Turning on incrementalMode together with resumeFromRunId is only allowed to bootstrap a brand-new monitoring baseline; if that state key already has tracked listings from an earlier run, the actor fails fast, before any scraping, and asks you to drop Resume from run ID or pick a different State key.
Send results into your apps (MCP connectors)
Beyond the dataset, this Actor can send a condensed summary of each listing straight into the tools you already use, via Model Context Protocol (MCP) connectors. The full record is always kept in the dataset; the connector receives a readable summary (title plus key fields) per item.
Supported connectors: Notion, Linear, Airtable, and Apify.
- Authorize the connector once under Apify → Settings → Integrations.
- Select one or more in the
mcpConnectorsinput. - (Notion only) Set
notionParentPageUrlto the page under which one page per listing is created. - Optionally cap how many listings are forwarded with
maxNotifyListings(default 50).
{"mode": "search","locations": ["madrid-capital"],"operation": "comprar","maxListings": 50,"mcpConnectors": ["notion"],"notionParentPageUrl": "https://www.notion.so/your-workspace/Listings-0000"}
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search (location + filters) or url (direct URLs). |
locations | array | no default (prefill: ["madrid-capital"]) | Place names or location slugs, e.g. Valencia, madrid-capital, barcelona-capital/centro. Search mode only; leave empty for all of Spain. |
operation | string | comprar | comprar (buy), alquiler (rent), compartir (share), opcion-compra (rent with option to buy) or traspaso (transfer). Search mode only. |
propertyType | string | viviendas | Property category, e.g. viviendas, pisos, casas, edificios (buildings, buy only). Search mode only. |
sortBy | string | relevance | relevance, newest, price_asc, price_desc, surface_asc or surface_desc. |
minPrice | integer | no default | Minimum price filter (EUR). Search mode only. |
maxPrice | integer | no default | Maximum price filter (EUR). Search mode only. |
minRooms | integer | no default | Minimum number of rooms. Search mode only. |
maxRooms | integer | no default | Maximum number of rooms. Search mode only. |
minBaths | integer | no default | Minimum number of bathrooms. Search mode only. |
maxBaths | integer | no default | Maximum number of bathrooms. Search mode only. |
minSurface | integer | no default | Minimum surface in square metres. Search mode only. |
maxSurface | integer | no default | Maximum surface in square metres. Search mode only. |
publicationDate | string | empty | Search mode only. last48h, lastWeek or lastMonth; applied to the scraped rows by each listing's publication instant (publicationDate). |
latitude | number | no default | Search mode only. Centre latitude for the radius filter; applied to the scraped rows. |
longitude | number | no default | Search mode only. Centre longitude for the radius filter. |
distanceKm | number | no default | Search mode only. Keep only listings within this distance (km) of the centre. |
rentalTypes | array | no default | Search mode only, rent only. Long-term and/or temporary; applied to the scraped rows. |
urls | array | one example of each supported URL shape | fotocasa.es URLs: search pages (city, zone, province, nationwide, rent, any property category, /l/N pages, sort and filter query strings), agency pages (/es/pro/<agency> and legacy inmobiliaria links) and single listing pages ending in /d (fetched once; with fetchDetails on the detail enrichment event is charged once for it, with it off there is no enrichment charge). URL mode only; filter fields above are ignored in this mode. |
fetchDetails | boolean | false | Adds energy certificate, full feature list, reference and agent/contact name and email from the listing detail page. Slower and increases cost per listing. |
maxListings | integer | 20 | Maximum total listings to scrape. This is the run's sole cap. Set 0 for unlimited. |
maxPages | integer | no default | Optional safety limit on result pages walked per search (~31 listings/page). Does NOT cap the run; leave empty (or 0) for unlimited and let Max items decide when to stop. |
resumeFromRunId | string | no default | Optional: paste a previous run id (or its dataset id) to continue that run. Listings already saved there are skipped, so this run only adds new ones. |
incrementalMode | boolean | false | Turn on for daily or recurring monitoring of the same search: classifies every listing NEW / UPDATED / UNCHANGED by default (REAPPEARED and EXPIRED need Emit expired listings too). |
stateKey | string | no default | Optional, incremental mode only. Names a monitoring campaign so its state stays stable, or so two differently-configured runs can deliberately share one baseline. |
emitUnchanged | boolean | false | Incremental mode only. Also return (and bill) listings unchanged since the last run, marked UNCHANGED. |
ignoreFieldsForChanges | array | [] | Incremental mode only. Fields (dotted paths allowed) that never make a listing UPDATED; they are still returned. |
changeScope | string | all | Incremental mode only. priceAndStock counts only price and availability changes. |
compactUpdates | boolean | false | Incremental mode only. Slim UPDATED and EXPIRED rows. |
expireAfterMissedScans | integer | 1 | Incremental mode only. Consecutive complete scans before EXPIRED. |
emitExpired | boolean | false | Incremental mode only. Also return (and bill) synthetic EXPIRED rows for tracked listings that no longer appear, produced only after a complete, uncapped scan of the tracked search. |
proxy | object | Residential proxy, Spain (ES) exit | Proxy used for all requests. Residential with an ES exit is the default; a datacenter group also works, with occasional per-exit refusals that are retried on a fresh exit. |
residentialCountries | array | ["ES"] | Country codes used for the residential proxy rotation. |
maxResidentialRequests | integer | 10000 | Caps how many requests may use the residential proxy in one run, to control cost. Once spent, the run continues on the standard Apify pool. Set 0 to disable the residential tier entirely. |
backupProxyUrl | string | no default | Optional. A full proxy URL used as a last-resort failover when the Apify proxy is unavailable (for example on a restricted plan). |
mcpConnectors | array | no default | Optional: send a summary of each listing to apps you authorized under Integrations. |
notionParentPageUrl | string | no default | Notion connector only: page under which one page per listing is created. |
maxNotifyListings | integer | 50 | Cap on listings forwarded to each connector per run. Does not affect the dataset. |
Output Example
Sample shape: values are illustrative placeholders, not from a live listing. Shown with
fetchDetailson, which is why the detail-only fields below are populated.
{"id": "1_000000000","propertyId": "000000000","title": "Calle de Ejemplo, Centro","rawPrice": 295000,"currency": "EUR","pricePerSqm": 4538.46,"surface": 65,"rooms": 3,"baths": 2,"floor": "4","propertyType": 2,"propertySubtype": 1,"operation": "sale","transactionTypeId": 1,"city": "Madrid","neighborhood": "Centro","address": { "locality": "Madrid", "neighborhood": "Centro", "coordinates": { "lat": 40.4168, "lng": -3.7038 } },"latitude": 40.4168,"longitude": -3.7038,"agencyName": "Example Agency SL","clientAlias": "Example Agency","clientType": "professional","phone": "+34000000000","reference": "REF-00000","energyCertificate": { "energyEfficiencyRatingType": "C" },"images": ["https://static.fotocasa.es/images/ads/0000?rule=original"],"photosCount": 12,"description": "Bright reformed home in a well-connected central area.","url": "https://www.fotocasa.es/es/comprar/vivienda/madrid-capital/centro/000000000/d"}
Plan Requirement
A paid Apify plan is not required for the pages themselves: the standard datacenter pool serves fotocasa.es search and listing pages, and the default setting is the Apify residential proxy with a Spain (ES) exit, which needs a plan that includes residential proxies. Fotocasa.es's edge occasionally refuses a single exit with a short CloudFront error page (in testing about 4 of 25 datacenter requests, and the same URL worked on the next exit), so the actor simply rotates to a fresh exit and retries; one refused exit never ends the run. Pick your own proxy group under Connection if you prefer one. If the selected connection is unavailable to your account, the log says "The selected connection is unavailable to this run" and the run continues on a backup connection ("Served over the backup connection"); some pages may then still be unavailable. If the proxy is refused outright the run fails with "The proxy connection was refused for this account"; a paid plan is then required, or supply your own proxy URL. Large or frequent runs should keep an eye on Residential request budget: once it is spent the run continues on the standard pool.
FAQ
How much does it cost?
You pay per run start, per listing returned, and an optional per-listing surcharge for detail enrichment only when fetchDetails is on (compact incremental rows carry no detail data and are not charged it). The Pricing tab shows the current rates. Use Max items to cap the cost of any run.
Is it legal to scrape Fotocasa.es?
This actor collects only publicly available property listing data. You are responsible for how you use it: follow fotocasa.es's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing facts like price and surface are generally usable, but photos, descriptions and agency branding may be subject to third-party rights.
Can I get only new or changed listings on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns only new and updated listings by default; unchanged ones are not returned or billed unless you turn on Emit unchanged listings, and reappeared listings only show up once you also use Emit expired listings.
What happens if I run URL mode without entering any URLs?
Behaviour change: the URLs field now ships with a default list holding one example of every supported URL shape, so a URL-mode run with the field untouched scrapes those examples (capped by Max items) instead of failing with "no URLs provided". Replace the list with your own URLs. A single listing URL (ending in /d) returns that one listing in full from one page fetch, and a search URL's sort and filter query strings apply on every result page. URLs that cannot be scraped (m.fotocasa.es, paths without /es/, /en/ or /ca/, obra-nueva pages other than the ".../obra-nueva/l" form) are skipped with a warning in the log; if none of the pasted URLs is supported the run fails with a message that names each rejected URL and the reason.
Which extra filters and operations are supported?
Search mode also accepts the operations Share, Rent with option to buy and Transfer (traspaso), and the Buildings property type (buy). Published within (last 48 hours, week or month), a radius around a latitude/longitude, and Rental types (long term / temporary, rent only) are applied to the scraped rows rather than on the site, so pair them with Sort By = Newest for the fullest coverage. Each row now also carries a top-level zipCode. Billing is unchanged: one dataset item per row delivered.
Which dataset views are there?
The dataset has four views: Listings Overview, Listing Detail (fetchDetails), Changes (incremental mode) and Agency & Contact. The views only choose which columns are shown; every field is always in the full dataset.
Why does a pasted "URL" search page ignore my filter fields?
In url mode the search-result URL fully defines the query, exactly as it was built on fotocasa.es, so the price/rooms/baths/surface filter fields in the input are ignored on purpose. Build the filters into the URL on the site itself, or switch to search mode to use the filter fields.
Why did my run fail instead of returning an empty dataset?
If every request to fotocasa.es was refused (rate-limited, blocked, or no readable response at all), the run fails with a clear message instead of silently completing as if the search itself came back empty, so "nothing was found" is never confused with "nothing could be read". The message tells apart a refused proxy connection (The proxy connection was refused for this account), residential access being unavailable (No results: residential proxies are unavailable on this run) and a failed page load (Could not load any results page: a connection problem, not an empty search). Retry after a few minutes, or choose a different proxy group under Connection. A genuinely empty search, on the other hand, still succeeds and reports quietly with no billable row; in URL mode the status message names each URL that returned nothing and why (listing removed or unavailable, page not found, no listings, or refused after retries).
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.
🔗 Want more real estate data?
Pair this actor with these related scrapers from the same team:
| 🏠 Immowelt.de Scraper Scrape detailed property listings from Immowelt.de into clean structured data. Extract... | 🏠 Idealista Scraper Scrape Idealista properties across Spain, Italy, and Portugal. Extract 35+ fields... |
| 🏠 Zonaprop Argentina Scrape Zonaprop.com.ar property listings across Argentina. Extract 140+ fields including... | 🏠 SeLoger Scraper Scrape SeLoger.com properties for sale and rent. Search by location with price, room and... |
| 🧩 Miravia Spain Scraper Scrape Miravia Spain by keyword, seller store or URL. Every row carries the selling price... | 🛒 Decathlon.es Scrape Decathlon Spain (decathlon.es) sporting goods: current price plus strike-through... |
💬 Support
- 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- 🛠️ Need another site, extra fields or a private build? Email contact@abotapi.com.
- ⭐ Enjoying it? A quick review on the actor page helps other users find it.