# PropertyFinder Scraper (UAE, Saudi, Qatar, Bahrain, Egypt) (`scrapyx/propertyfinder-properties-scraper`) Actor

Scrapes property listings from PropertyFinder across five Gulf and Egyptian markets. Each row carries price, area, bedrooms, address with GPS coordinates, property type, completion status, agent and agency, RERA permit data and photos. Search plus optional full listing details.

- **URL**: https://apify.com/scrapyx/propertyfinder-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

## PropertyFinder Scraper — UAE, Saudi Arabia, Qatar, Bahrain, Egypt

Scrapes property listings from **PropertyFinder**, the largest property portal
in the Gulf, across five markets from one Actor:

| Market | Site | Example scale (residential for sale) |
| --- | --- | --- |
| United Arab Emirates | propertyfinder.ae | ~187,000 listings |
| Egypt | propertyfinder.eg | ~182,000 listings |
| Qatar | propertyfinder.qa | ~10,600 listings |
| Bahrain | propertyfinder.bh | ~8,900 listings |
| Saudi Arabia | propertyfinder.sa | ~4,400 listings |

Residential and commercial, for sale and for rent, with optional full listing
details including RERA permit data.

***

### What each row contains

Every listing is emitted as upstream returns it, so nothing is lost to
renaming. Highlights:

- **Price** — value, currency and period, plus price per area
- **Size** — value and unit, plus service fee per sqft (details mode)
- **Bedrooms / bathrooms / rooms**
- **Location** — full name, slug, **GPS coordinates**, and the complete
  `location_tree` from city → community → subcommunity → tower, each with its
  own id you can feed back in as a narrower query
- **Property type**, offering type, completion status (ready / off-plan),
  furnishing
- **Agent** — name, photo, licence number (details mode), super-agent flag
- **Agency / broker** — name, logo, address
- **RERA / regulatory data** — permit numbers and the full regulatory table
  (details mode)
- **Reference**, listed date, verification and quality flags
- **All photo URLs** and image count

#### Record types

One dataset, three `recordType` values:

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

***

### Honesty flags — read these before trusting a filtered run

PropertyFinder answers several kinds of bad input with **HTTP 200 and
plausible-looking data** rather than an error. This Actor detects each case and
reports it rather than passing the wrong data off as right.

- **`locationApplied`** — a location id PropertyFinder cannot resolve is not
  rejected; it silently returns *the entire country*. When this is `false` the
  Actor stops **before** paginating and emits **zero** property rows, so a
  mistyped location can never quietly fill your dataset with the wrong city.
- **`propertyTypeApplied`** — an unknown numeric type returns a page containing
  zero real listings. Types are validated up front against the list each market
  publishes for itself.
- **`categoryBaselineTotal`** — the unfiltered total for the same category and
  type. Compare it with `totalMatches` to see for yourself that your filters
  actually narrowed the search.
- **`promoCardsSkipped`** — results pages interleave developer advertising
  cards with real listings. Those are counted here and never emitted as
  properties.

***

### Input

| Field | Notes |
| --- | --- |
| `country` | `ae`, `sa`, `qa`, `bh`, `eg` |
| `locations` | City **or community** names (`Dubai`, `Dubai Marina`, `New Cairo City`), or numeric ids for any level. Empty = whole market |
| `category` | `buy`, `rent`, `commercial-buy`, `commercial-rent` |
| `propertyType` | Slug, label or id — e.g. `apartment`, `villa`, `office-space`. Empty = all |
| `bedrooms` | Multi-select, `0` = studio |
| `minPrice` / `maxPrice` | Market's own currency |
| `minArea` / `maxArea` | sqft |
| `keywords` | Free text across title and description |
| `furnished` | furnished / unfurnished / partly furnished |
| `completionStatus` | `completed` (ready) or `off_plan` |
| `listedWithin` | 24 hours … 90 days |
| `sort` | featured, newest, price, beds |
| `includePropertyDetails` | One extra request per listing; adds permit/regulatory data |
| `maxItems` | Per location |

Every filter offered here was verified to genuinely change the result count.
Filters that PropertyFinder silently ignores are **deliberately absent** —
notably **bathrooms**, for which the site publishes a full dropdown in its UI
but accepts no working URL parameter (six candidate spellings all returned the
unfiltered total).

#### Finding narrower areas

Run once broadly, then read `location.id` or `location_tree` off any row and
use that id as a `locations` entry. Community names are resolved
automatically for the largest cities in each market; ids work at every level
including individual towers.

***

### Example

```json
{
  "country": "ae",
  "locations": ["Dubai Marina", "Business Bay"],
  "category": "buy",
  "propertyType": "apartment",
  "bedrooms": ["2", "3"],
  "minPrice": 1000000,
  "completionStatus": "completed",
  "sort": "newest",
  "includePropertyDetails": true,
  "maxItems": 200
}
```

***

### Known limits

- **Result window.** A single query pages through thousands of results, but
  broad queries are enormous (the UAE apartment search alone reports ~6,000
  pages). Narrow by city, community, type or price rather than crawling a whole
  market — several narrow queries return far more usable data than one wide one.
- **A cost guard stops one query at 400 pages.** Raise `maxItems` and narrow
  the query to go deeper.
- **`totalMatches` is read from page 1 only.** The live index shifts under a
  running crawl, so later pages report slightly different totals.
- **Turkey (propertyfinder.com.tr) is not supported.** It runs a different
  front end with no embedded data.
- **New Projects** is not a category here. That surface returns developer
  project cards rather than individual listings.
- Listings de-listed mid-run return 404 on the detail pass. The search row is
  still emitted, with `detailFetched: false`.

### Anti-bot

None encountered. Eight TLS fingerprints were tested against search pages,
paginated search pages and listing detail pages — all 24 combinations returned
data cleanly, with no warmup, cookies or tokens. 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

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

Which PropertyFinder market to scrape. All five run the same front end, so every option returns the same fields.

Turkey (propertyfinder.com.tr) is **not** offered: it serves a different front end with no embedded data, so it cannot be read the same way.

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

One city/region per entry, each producing its own SEARCH\_SUMMARY row — e.g. `Dubai`, `Abu Dhabi`, `Cairo`, `Riyadh`. **Leave empty to scrape the whole market in one query.**

Names are matched against the city breakdown PropertyFinder itself publishes for the chosen market, so only real ones are accepted. If a name is not found, the run reports the available cities rather than guessing.

For a smaller area than a city — a community like Business Bay, or a single tower — pass its **numeric location id** instead. Those ids are on every result row under `location.id` and `location_tree`, so one broad run tells you the ids for narrower follow-up runs.

Why this is strict: an id PropertyFinder cannot resolve is **not** an error upstream — it silently returns the entire country as though no filter were set.

## `category` (type: `string`):

Which side of the market to scrape. Residential and commercial have **different property-type lists**, so choose this first.

`New Projects` is deliberately absent — that surface returns developer project cards rather than individual listings, which is a different data shape.

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

Restrict to one property type, e.g. `apartment`, `villa`, `townhouse`, `penthouse`, `compound`, `duplex`, `land`, `whole-building`. For commercial categories use `office-space`, `retail`, `warehouse`, `shop`, `show-room`, `factory`. Leave empty for all types.

Accepts the slug, the display label, or the raw numeric id. The value is checked against the type list **that market and category publishes for itself** — the same numeric id means different things in different markets (id 22 is Townhouse in the UAE but Farm in Saudi Arabia), so nothing is hardcoded.

An unrecognised type is refused before the crawl starts, and the error lists what that market actually offers. This matters: PropertyFinder answers an unknown numeric type with HTTP 200 and a page containing zero real listings.

## `bedrooms` (type: `array`):

Match any of the selected bedroom counts. `0` means studio. Leave empty for any.

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

In the market's own currency (AED, SAR, QAR, BHD, EGP). Verified real: `5000000` narrowed the UAE apartment baseline from 123,441 to 10,416. Leave at 0 to skip.

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

In the market's own currency. Verified real: `500000` narrowed the UAE apartment baseline from 123,441 to 5,766. Leave at 0 to skip.

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

Verified real: `2000` narrowed the UAE apartment baseline from 123,441 to 11,382. Leave at 0 to skip.

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

Verified real: `800` narrowed the UAE apartment baseline from 123,441 to 44,955. Leave at 0 to skip.

## `keywords` (type: `string`):

Free-text search across listing titles and descriptions, e.g. `marina`, `sea view`, `maid room`. Verified real, and honest about misses — nonsense text returns a genuine 0 rather than the unfiltered baseline.

## `furnished` (type: `string`):

Leave empty for any. Note there is no separate 'any' value to send — omitting the filter is what 'any' means.

## `completionStatus` (type: `string`):

Ready (completed) or off-plan. Verified real: on the UAE apartment baseline of 123,441 these split 68,325 / 68,664.

Only these two are accepted. A plausible-looking third value such as `under_construction` is **silently ignored** by PropertyFinder — it returns the unfiltered total while echoing the value back — so it is refused here rather than reported as applied.

## `listedWithin` (type: `string`):

Only listings published inside this window. Verified real: 7 days narrowed the UAE apartment baseline from 123,441 to 17,815.

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

## `sort` (type: `string`):

All six were verified to genuinely reorder results (each returns a different first listing), not silently ignored.

Use `Newest` when scraping repeatedly to track new stock — it puts fresh listings on page 1.

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

Fetch each listing's own page for fields the results list does not carry: the RERA/permit and property-detail tables, agent licence numbers, service fee per sqft, the untruncated description, project delivery date and payment plan, and rental availability date.

Costs **one extra request per listing**, so leave it off for broad market sweeps and turn it on for a shortlist. Listings de-listed mid-run answer 404 — those rows are still returned, with `detailFetched: false`.

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

Stop after this many listings per location. Set to 0 for everything PropertyFinder will serve — which can be six thousand pages for a whole market, so prefer narrowing by city, type or price.

## `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. PropertyFinder showed no rate limiting in testing, so this defaults to 0; raise it if you run long unattended crawls.

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

Residential by default. PropertyFinder showed no bot challenge on any of the 8 TLS fingerprints tested across search and detail pages, 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 — so residential is the safe default.

## Actor input object example

```json
{
  "country": "ae",
  "locations": [
    "Dubai"
  ],
  "category": "buy",
  "propertyType": "",
  "bedrooms": [],
  "minPrice": 0,
  "maxPrice": 0,
  "minArea": 0,
  "maxArea": 0,
  "keywords": "",
  "furnished": "",
  "completionStatus": "",
  "listedWithin": "0",
  "sort": "featured",
  "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": [
        "Dubai"
    ]
};

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

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

```

## MCP server setup

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