Web Scraping Layers for AI & Automation

back to blog

Building a Multi-Market European Real Estate Data Pipeline — Funda, Fotocasa, and Beyond

ParseBird·04 Sep 2026

Key Takeaways

Why isn't there a single API for European property listings? Real estate portals are national or regional by nature — each country's dominant platform grew independently, with its own listing conventions, currency, and legal disclosure requirements (energy labels in the Netherlands, cadastral references in Spain). No portal has incentive to standardize against competitors in other markets.

What's the minimum normalized schema across markets? Price (converted to one currency), living area in a consistent unit (m²), location (city + country), listing URL, and publication date. Everything market-specific — Dutch energy labels, Spanish cadastral data, UAE broker licensing — becomes optional enrichment layered on top, not part of the core comparison schema.

Which of these sources are keyword/city-driven versus URL-seeded? Funda and Fotocasa accept a location string directly as an input parameter. Propertyfinder is URL-seeded — you paste a Propertyfinder search-result URL rather than passing city/price filters as separate inputs, which changes how you'd script a multi-city pull.

Four Markets, Four Schemas

Property portals didn't converge on a shared format because they never had to — each dominates a single national market with no cross-border competitor forcing standardization. Four sources cover the ground here: Funda.nl Scraper for the Netherlands, Fotocasa Scraper for Spain, Propertyfinder Scraper for the UAE and wider MENA region, and VivaReal Property Scraper for Brazil. Side by side, the same underlying concept (a listing's price and size) comes back shaped completely differently:

// funda-scraper — Netherlands
{
  "AddressTitle": "Waalstraat 122", "AddressSubTitle": "1079 EC Amsterdam",
  "Price": { "SellingPrice": "€ 595.000 k.k.", "NumericSellingPrice": 595000 },
  "WoonOppervlakteSubTitle": "72 m²", "NumberOfBedrooms": "1",
  "BuurtName": "Scheldebuurt-Midden"
}

// fotocasa-scraper — Spain
{
  "propertyId": "187123129",
  "transaction": { "type": "SALE", "price": 120000 },
  "surface": 57, "rooms": 3,
  "address": { "district": "Centro", "municipality": "Madrid", "province": "Madrid" }
}

// propertyfinder-scraper — UAE / MENA
{
  "property_type": "Apartment",
  "price": { "value": 24000, "currency": "AED", "period": "yearly" },
  "location": { "full_name": "Al Butina B, Al Butina, Sharjah" },
  "size": { "value": 900, "unit": "sqft" }
}

// vivareal-property-scraper — Brazil
{
  "pricing": { "amount": 295000, "currency": "BRL" },
  "location": { "neighborhood": "Irajá", "city": "Rio de Janeiro", "state_code": "RJ" },
  "attributes": { "area": { "usable_area": 98 } }
}

Four different nesting structures, two different area units (m² versus sqft), four different currencies, and Propertyfinder's price is even a rental rate by default (period: "yearly") rather than a sale price — a detail that will silently corrupt a cross-market comparison if you don't check transaction.type / property_type context per source before treating every price as directly comparable.

Two Different Input Patterns

Before writing a puller, know which sources take structured search parameters and which require a seed URL — this changes how you'd script pulling multiple cities:

// Funda — parameter-driven, easy to loop over a list of cities
const fundaInput = {
  searchLocation: "amsterdam",
  searchTransactionType: "buy",
  searchPriceMin: 400000,
  searchPriceMax: 700000,
  maxItems: 100,
};

// Propertyfinder — URL-seeded, you generate the search URL per city yourself first
const propertyfinderInput = {
  startUrl: [
    { url: "https://www.propertyfinder.ae/en/search?l=1&c=1&t=1&fu=0&rp=y&pf=200000&pt=800000" },
  ],
  maxItems: 100,
};

If you're scripting a loop across cities, Funda and Fotocasa's parameter-driven inputs are trivial to iterate — swap searchLocation/location per run. Propertyfinder needs a URL-builder step first (constructing the query-string search URL per city/filter combination) before you can loop the same way.

Normalizing to One Comparable Schema

const SQFT_TO_SQM = 0.0929;
const FX_TO_EUR = { EUR: 1, AED: 0.25, BRL: 0.17 };

function normalizeListing(record, source) {
  switch (source) {
    case "funda":
      return {
        priceEur: record.Price.NumericSellingPrice,
        areaSqm: parseFloat(record.WoonOppervlakteSubTitle),
        city: record.AddressTitle,
        country: "NL",
      };
    case "fotocasa":
      return {
        priceEur: record.transaction.price,
        areaSqm: record.surface,
        city: record.address.municipality,
        country: "ES",
      };
    case "propertyfinder":
      return {
        priceEur: record.price.value * (FX_TO_EUR[record.price.currency] ?? 1) *
                   (record.price.period === "yearly" ? 1 / 12 : 1), // normalize to monthly if it's a rental
        areaSqm: record.size.unit === "sqft" ? record.size.value * SQFT_TO_SQM : record.size.value,
        city: record.location.full_name,
        country: "AE",
      };
    case "vivareal":
      return {
        priceEur: record.pricing.amount * FX_TO_EUR[record.pricing.currency],
        areaSqm: record.attributes.area.usable_area,
        city: record.location.city,
        country: "BR",
      };
  }
}

Once normalized, priceEur / areaSqm gives you a genuinely comparable price-per-square-meter figure across all four markets — which is the actual metric relocation services and cross-border proptech tools need, and which none of these portals expose natively since none of them are built to be compared against each other.

Who Actually Needs This

This pipeline isn't for a single-market agent — it's for relocation services helping someone compare "what does my budget get me in Amsterdam versus Dubai versus Rio," cross-border proptech products, or investors screening multiple markets for yield. If your use case is a single market, just use that market's actor directly rather than building the normalization layer at all — it's only worth the effort once you're actually comparing across borders.

FAQ

Do these actors handle rental listings the same way as sales? Not uniformly — check each source's transaction.type or property_type field per record. Propertyfinder defaults to rental listings unless you specify otherwise in the search URL; Funda and Fotocasa both expose an explicit buy/rent input parameter.

What about markets not covered here — Germany, France, Italy? The same normalization pattern extends to any additional portal actor — the point of this pipeline is the normalization layer, not the specific four markets shown. Add a case to the switch statement per new source.

For a similar lead-qualification pattern in a different real estate niche, see Finding Distressed Property Leads Before They Hit the MLS, or browse ParseBird's real estate actors for additional markets.