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.
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
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:
- Runs the same search (1-bedroom rentals in Dubai Marina) on both portals.
- Turns each portal’s listings into one shape, so the rest of your code does not care where a listing came from.
- Finds listings that appear on both portals, using the agency’s reference number.
- 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 and put your key in the
RAPIDAPI_KEY environment variable. Then start compare_portals.py with a small client:
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=otheron RapidAPI (platform=bayutis accepted too), and Bayut responses say"platform": "other". Treatotheras Bayut everywhere in your code. - Every response is wrapped in
{"success", "platform", "data"}. A failed call returns"success": falsewith anerrorcode and amessage, soget()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:
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.
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:
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:
# 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")
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.
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, withdata.nbHitsfor the total anddata.nbPagesfor the page count. Every value is a string, so convert before doing arithmetic.contract_monthly_amounttimes 12 gives the annual rent,rent_contract_renewal_status_namesays whether it is a new lease or a renewal. - Property Finder returns
data.data.attributeswithtransactions(15 a page),total_itemsand asummaryof the whole set: average new-lease rent (rent_new_avg_price), average renewal rent (rent_renew_avg_price) and a gross yield (roi).
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:
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:
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.
Where to go next
- 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: endpoints, an example response and which of our UAE APIs fits your project.
- Runnable examples on GitHub: 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. Need only Bayut? See BayutAPI.