# Bayut and Property Finder in Python: One API for Both Portals

> Search Bayut and Property Finder from one Python script: put both portals' listings in one shape, find flats listed on both, and pull registered rents.

Source: https://uaepropertyapi.com/guides/bayut-property-finder-python

## What you will build

Bayut and Property Finder are the two big property portals in the UAE. Most agencies list on both, but not every flat
is on both, and the portals return their data in different shapes. The [UAE Real Estate API](/uae-real-estate-api)
puts both behind one key and one set of endpoints: every request takes a `platform` parameter, and switching portals
is one parameter change.

In this guide you write one Python script, `compare_portals.py`, that:

1. Runs the same search (1-bedroom rentals in Dubai Marina) on both portals.
2. Turns each portal's listings into one shape, so the rest of your code does not care where a listing came from.
3. Finds listings that appear on both portals, using the agency's reference number.
4. Pulls registered rental contracts for the same area with `/get-transactions`, so you can set asking rents against
   what tenants signed.

Every output shown below comes from a run on 12 November 2026. Live numbers change daily.

## Set up the client

Install `requests` (`pip install requests`), subscribe to the
[UAE Real Estate API on RapidAPI](https://rapidapi.com/happyendpoint/api/uae-real-estate-api) and put your key in the
`RAPIDAPI_KEY` environment variable. Then start `compare_portals.py` with a small client:

```python
import os
import statistics

import requests

HOST = "uae-real-estate-api.p.rapidapi.com"
BASE = f"https://{HOST}"
HEADERS = {"x-rapidapi-key": os.environ["RAPIDAPI_KEY"], "x-rapidapi-host": HOST}
PORTALS = {"other": "Bayut", "propertyfinder": "Property Finder"}  # Bayut is "other" on RapidAPI
SQFT_PER_SQM = 10.7639


def get(path, platform, **params):
    res = requests.get(f"{BASE}{path}", headers=HEADERS,
                       params={"platform": platform, **params}, timeout=60)
    body = res.json()
    if not body.get("success"):
        raise RuntimeError(f"{PORTALS[platform]} {path}: {body.get('error')} {body.get('message')}")
    return body["data"]
```

Two things to know about `platform`:

- Bayut is `platform=other` on RapidAPI (`platform=bayut` is accepted too), and Bayut responses say `"platform": "other"`.
  Treat `other` as Bayut everywhere in your code.
- Every response is wrapped in `{"success", "platform", "data"}`. A failed call returns `"success": false` with an
  `error` code and a `message`, so `get()` raises with both instead of handing you an empty result.

## Put both portals' listings in one shape

`/search-properties` returns the same envelope on both portals (`properties`, `total`, `page`, `totalPages`), but the
listing records inside differ:

| Field | Bayut (`platform=other`) | Property Finder |
|---|---|---|
| Listing id | `externalID` | `property_id` |
| Price | `price` (a number) | `price.value` (an object with `currency` and `period`) |
| Bedrooms | `rooms` (a number) | `bedrooms` (a string) |
| Size | `area`, in square metres | `size.value`, in square feet |
| Title | `title.en` | `title` |
| Agency reference | `referenceNumber` | `reference_number` |

Property Finder also mixes new-project promotions into search results. Those rows have `listing_type` set to
`project`, no price and a list of bedroom options, so skip them. A listing with a hidden price has a `price` object
without a `value`.

This function turns a row from either portal into one dictionary:

```python
def normalise(platform, row):
    """One shape for a listing from either portal."""
    if platform == "other":  # Bayut: area in square metres, title as {"en": ...}
        return {
            "portal": "Bayut", "id": row["externalID"], "ref": row.get("referenceNumber"),
            "price": row["price"], "beds": str(row["rooms"]),
            "sqft": round(row["area"] * SQFT_PER_SQM) if row.get("area") else None,
            "title": row["title"]["en"],
        }
    if row.get("listing_type") != "property":  # Property Finder mixes new-project promos into results
        return None
    return {  # Property Finder: price and size are objects; a hidden price has no value
        "portal": "Property Finder", "id": row["property_id"], "ref": row.get("reference_number"),
        "price": (row.get("price") or {}).get("value"), "beds": row["bedrooms"],
        "sqft": row["size"]["value"] if row.get("size") else None,
        "title": row["title"],
    }
```

Keep the portal's own id in `id`: it is the value `/property-details` takes as `property_id` on that portal.

## Search the same area on both portals

Pass the area by name with `location`. Location IDs differ between the portals (Dubai Marina is 5003 on Bayut and 50
on Property Finder), and Property Finder's `/search-properties` needs `location`: a `location_id` alone returns a
validation error. With a name, the API resolves the right ID on each portal for you.

```python
def listings(platform, area, pages=4, **filters):
    total, rows = 0, []
    for page in range(1, pages + 1):
        data = get("/search-properties", platform, location=area, page=page, **filters)
        total = data["total"]
        rows += [n for n in (normalise(platform, r) for r in data["properties"]) if n]
        if page >= data["totalPages"]:
            break
    return total, rows


AREA, FILTERS = "Dubai Marina", {"purpose": "rent", "bedrooms": "1", "rent_frequency": "yearly"}

found = {}
for platform, name in PORTALS.items():
    total, rows = listings(platform, AREA, **FILTERS)
    found[platform] = rows
    prices = [r["price"] for r in rows if r["price"]]
    print(f"{name:16} {total:>6,} listings | median asking rent of {len(prices)} fetched: "
          f"AED {statistics.median(prices):,.0f}")
```

Output on 12 November 2026:

```text
Bayut             2,177 listings | median asking rent of 100 fetched: AED 89,450
Property Finder   2,280 listings | median asking rent of 135 fetched: AED 95,000
```

`total` is the count for the whole search. The medians cover only the listings fetched (four pages, in each portal's
default order), so treat them as a check that the code works, not as the market rent. Bayut pages hold 25 listings;
Property Finder pages hold more, which is why four pages gave 135 priced listings there. To measure the area, page
through all of it, or see the full comparison below.

## Find listings that are on both portals

The same flat is often listed on both portals by the same agency, with the same reference number. Matching on the
reference and the price finds those pairs:

```python
# The same flat is often listed on both portals by the same agency, with the same reference.
keys = {p: {(r["ref"], r["price"]) for r in rows if r["ref"]} for p, rows in found.items()}
both = keys["other"] & keys["propertyfinder"]
print(f"Same reference and price on both portals: {len(both)} of {len(keys['other'])} fetched Bayut listings")
```

```text
Same reference and price on both portals: 12 of 100 fetched Bayut listings
```

This counts only pairs that are both inside the pages you fetched, so it is a floor, not the real overlap. Agencies
also do not always use the same reference on both portals. Fetch every page of both searches to measure the overlap
for an area. Across every live listing in Dubai, at least 31 to 38% were on both portals on 10 November 2026; the
method is in the [Bayut vs Property Finder comparison](/compare/bayut-vs-property-finder).

When you merge both portals into one list, drop one listing of each pair so the same flat is not counted twice.

## Add registered rents with /get-transactions

Asking rents are what landlords want. Registered rental contracts (Ejari) are what tenants signed. `/get-transactions`
returns them on both portals, with the same parameters (`transaction_type`, `location`, `time_period`, `bedrooms`,
`property_type`), but the two portals return different shapes:

- **Bayut** returns the contracts themselves: `data.hits`, with `data.nbHits` for the total and `data.nbPages` for the
  page count. Every value is a string, so convert before doing arithmetic. `contract_monthly_amount` times 12 gives the
  annual rent, `rent_contract_renewal_status_name` says whether it is a new lease or a renewal.
- **Property Finder** returns `data.data.attributes` with `transactions` (15 a page), `total_items` and a `summary` of
  the whole set: average new-lease rent (`rent_new_avg_price`), average renewal rent (`rent_renew_avg_price`) and a
  gross yield (`roi`).

```python
def registered_rents(platform, area, **filters):
    """Registered (Ejari) rental contracts in the last 12 months: count and a summary line."""
    data = get("/get-transactions", platform, transaction_type="rental",
               location=area, time_period="1y", **filters)
    if platform == "other":  # Bayut: Algolia-style rows (hits), every value is a string
        rents = [float(h["contract_monthly_amount"]) * 12 for h in data["hits"]
                 if h.get("contract_monthly_amount")]
        return data["nbHits"], f"median of the {len(rents)} newest: AED {statistics.median(rents):,.0f}"
    tx = data["data"]["attributes"]  # Property Finder: rows plus a summary of the whole set
    s = tx["summary"]
    return tx["total_items"], (f"average new lease AED {s['rent_new_avg_price']:,.0f}, "
                               f"renewal AED {s['rent_renew_avg_price']:,.0f}")


for platform, name in PORTALS.items():
    try:
        count, line = registered_rents(platform, AREA, bedrooms="1", property_type="apartment")
        print(f"{name:16} {count:>6,} registered contracts | {line}")
    except (RuntimeError, requests.RequestException) as err:
        print(f"{name:16} transactions not available right now ({err})")
```

The Property Finder line on 12 November 2026:

```text
Property Finder   4,204 registered contracts | average new lease AED 99,394, renewal AED 93,702
```

Note the difference between the two kinds of number. The listing medians above describe what is on offer today; the
contract figures describe leases signed over the last 12 months, and averages are pulled up by the most expensive flats.
Compare like with like: a median against a median, the same bedroom count, the same period.

The `try` block matters here. Transaction calls depend on the portal's own transaction service. When it does not
answer, the call for that portal fails with `PLATFORM_ERROR` ("Service temporarily unavailable") or times out, and the
other portal is not affected. Catching the error per portal keeps the other result. On our run, Bayut's transaction
service did not answer, so only the Property Finder line is shown; retry later rather than in a tight loop.

## Run it

The script is the five code blocks above, in order, in one file:

```bash
export RAPIDAPI_KEY=your_key
python compare_portals.py
```

Each run makes up to 10 calls: four search pages on each portal and one transactions call on each. Change `AREA` and
`FILTERS` to compare another area, for example `{"purpose": "sale", "bedrooms": "2"}` for 2-bedroom flats for sale.

## Errors you will meet

| Response | What it means | What to do |
|---|---|---|
| `PLATFORM_LIMITATION` | The endpoint exists for one portal only. `/agent-details` and `/agency-details` are Bayut only | Use `/search-agents`, `/agent-properties`, `/search-agencies` and `/agency-properties`, which work on both |
| Validation error, "PropertyFinder requires 'location' parameter" | A Property Finder search was sent with `location_id` only | Send `location` (the area name) |
| `PLATFORM_ERROR` | That portal's service did not answer | Retry later; the other portal is not affected |
| `NOT_FOUND` | The id does not exist on that portal | Check you used that portal's id (`externalID` on Bayut, `property_id` on Property Finder) |

Every endpoint, parameter and response shape for both portals is in the
[UAE Real Estate API documentation](/documentation/uae-real-estate-api).

## Where to go next

- [Bayut vs Property Finder](/compare/bayut-vs-property-finder): the same comparison across every live listing in the
  UAE, with median asking rents in 18 Dubai areas.
- [UAE Real Estate API](/uae-real-estate-api): endpoints, an example response and which of our UAE APIs fits your project.
- [Runnable examples on GitHub](https://github.com/happyendpointhq/uae-real-estate-api): a reusable client, a
  transactions-to-CSV export and an asking-versus-paid example.
- Need only Property Finder, with price trends, new projects and DLD transaction records? Use the
  [Property Finder API](/documentation). Need only Bayut? See [BayutAPI](https://bayutapi.dev/?utm_source=uaepropertyapi&utm_medium=referral&utm_campaign=guide-bayut-property-finder-python).
