Property Finder CRM Integration: Sync Listings, Agents & Brokers into Your CRM
“Property Finder CRM integration” can mean two different things, and it is worth separating them before you write any code.
- Getting your own agency’s Property Finder leads and listing stats into your CRM. That data lives in your agency’s Property Finder account and comes through Property Finder’s own tools and official partner integrations.
- Getting market data from Property Finder into your CRM: live listings, agents, brokerages and transaction history across Dubai and the rest of the UAE. This is what UAEPropertyAPI does.
This guide is about the second one. (UAEPropertyAPI is an independent, third-party API, not affiliated with Property Finder; see the disclaimer.) It shows how to sync Property Finder listing, agent and broker data into a CRM such as HubSpot, Zoho or Salesforce, or into your own database, with a daily job in Python.
What you can (and can’t) sync with the API
Be clear about this up front, because it decides what you build.
You can sync:
- Listings for rent and sale, residential and commercial, filtered by area, property type, bedrooms, price and more (/search-rent, /search-buy, /search-commercial-rent, /search-commercial-buy).
- Full listing details: description, amenities, RERA permit, agent and brokerage (/property-details).
- Agents in an area and the listings each one holds (/search-agents, /agent-properties).
- Brokerages in an area and their listings (/search-brokers, /broker-properties).
- Off-plan projects (/search-new-projects).
- Market context: DLD rental and sale transactions and price trends for an area (/get-transactions, /price-trend-of-location).
You can’t sync:
- Leads or enquiries sent to your agency through Property Finder.
- Your agency’s private account data: listing performance, credits, call tracking.
- Publishing or editing listings on Property Finder.
The API returns the public market as seen on Property Finder. If you need your own leads in your CRM, use your Property Finder account’s lead tools. If you need to know what the whole market is listing, who is listing it and at what price, read on.
What teams use this for
- Inventory intelligence for agents. When a client asks for a 2-bed in Dubai Marina under AED 150,000, the agent sees every matching listing in the CRM, not just their own stock.
- Prospecting brokerages and agents. Brokerages and developers build target lists of active agents by area and category, with listing counts as a signal of activity.
- Owner and landlord outreach. Track price drops and long-listed properties in your patch, so agents know which landlords might switch.
- Valuation context on deals. Attach recent DLD transaction averages for the area to each deal record.
Architecture: API → staging table → CRM
Don’t write straight from the API into your CRM. Put a small staging database in between:
- A daily job pulls listings for the areas you care about.
- Each listing is upserted into a staging table keyed on
property_id. - Only changes (new listings, price changes, listings that disappeared) are pushed to the CRM.
This keeps your CRM clean, stops duplicates, and saves API requests, because you only enrich listings that are new or changed.
Step 1: Set up the client and resolve location IDs
Every search takes a numeric location_id. Resolve area names once with /autocomplete-location and store the IDs. Common ones: Dubai 1, Dubai Marina 50, Downtown Dubai 41, Business Bay 36, JVC 73.
import os
import requests
HOST = "uae-real-estate-property.p.rapidapi.com"
HEADERS = {
"x-rapidapi-host": HOST,
"x-rapidapi-key": os.environ["RAPIDAPI_KEY"],
}
def get(path, **params):
res = requests.get(f"https://{HOST}{path}", headers=HEADERS, params=params, timeout=30)
res.raise_for_status()
body = res.json()
if not body.get("success"):
raise RuntimeError(f"{path} failed: {body.get('error')}")
return body
# Resolve an area name to its Property Finder location ID
loc = get("/autocomplete-location", query="Dubai Marina")["data"][0]
print(loc["id"], loc["name"]) # 50 Dubai Marina
Step 2: Pull every listing with pagination
Listing searches return {"success": true, "total_count": ..., "data": [...]} with about 38 listings per page. page starts at 1. Keep paging until you’ve seen total_count listings or a page comes back empty.
def search_all(endpoint, location_id, max_pages=100, **filters):
"""Yield every listing for a search, page by page."""
seen, page = 0, 1
while page <= max_pages:
body = get(endpoint, location_id=location_id, page=page, **filters)
items = body["data"]
if not items:
break
yield from items
seen += len(items)
if seen >= body["total_count"]:
break
page += 1
rentals = list(search_all("/search-rent", 50, property_type="apartment", sort="newest"))
print(len(rentals), "apartment rentals in Dubai Marina")
Each listing carries what a CRM needs: property_id, title, property_type, price (value, currency, period), bedrooms (a string: "studio", "1", "2"…), bathrooms, size (value, unit), address.full_name, agent_details (id, name), broker_id, is_verified, listed_date and property_url.
Step 3: Upsert into a staging table keyed on property_id
property_id is stable for the life of a listing, so use it as the primary key. The upsert below records when a listing was first and last seen, and flags price changes.
import sqlite3
from datetime import date
db = sqlite3.connect("pf_sync.db")
db.execute("""
CREATE TABLE IF NOT EXISTS listings (
property_id TEXT PRIMARY KEY,
offering TEXT, -- rent or sale
title TEXT,
property_type TEXT,
bedrooms TEXT,
price INTEGER,
price_period TEXT,
address TEXT,
agent_id TEXT,
broker_id TEXT,
url TEXT,
first_seen TEXT,
last_seen TEXT,
price_changed TEXT,
removed INTEGER DEFAULT 0
)""")
def upsert(p, offering, today):
"""Insert or update one listing. Returns 'new', 'price_change' or 'seen'."""
price = p["price"]["value"]
old = db.execute("SELECT price FROM listings WHERE property_id = ?",
(p["property_id"],)).fetchone()
if old is None:
db.execute("""INSERT INTO listings (property_id, offering, title, property_type,
bedrooms, price, price_period, address, agent_id, broker_id, url,
first_seen, last_seen) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""", (
p["property_id"], offering, p["title"], p["property_type"], p["bedrooms"],
price, p["price"]["period"], p["address"]["full_name"],
p["agent_details"]["id"], p["broker_id"], p["property_url"], today, today))
return "new"
if old[0] != price:
db.execute("""UPDATE listings SET price = ?, price_changed = ?, last_seen = ?, removed = 0
WHERE property_id = ?""", (price, today, today, p["property_id"]))
return "price_change"
db.execute("UPDATE listings SET last_seen = ?, removed = 0 WHERE property_id = ?",
(today, p["property_id"]))
return "seen"
Step 4: Run the daily sync and push changes to the CRM
The job below syncs rent and sale listings for a few areas, then sends only new listings and price changes to the CRM. Listings not seen for 3 days are marked as off the market.
AREAS = {"Dubai Marina": 50, "Downtown Dubai": 41, "Business Bay": 36}
CRM_WEBHOOK = os.environ["CRM_WEBHOOK_URL"] # your CRM, Zapier/Make hook or own service
def push(event, p):
requests.post(CRM_WEBHOOK, timeout=15, json={
"event": event,
"property_id": p["property_id"],
"title": p["title"],
"price": p["price"]["value"],
"price_period": p["price"]["period"],
"bedrooms": p["bedrooms"],
"address": p["address"]["full_name"],
"agent_id": p["agent_details"]["id"],
"broker_id": p["broker_id"],
"url": p["property_url"],
})
def daily_sync():
today = date.today().isoformat()
for name, location_id in AREAS.items():
for endpoint, offering in (("/search-rent", "rent"), ("/search-buy", "sale")):
for p in search_all(endpoint, location_id, sort="newest"):
status = upsert(p, offering, today)
if status != "seen":
push(status, p)
db.commit()
# Anything not seen for 3 days has most likely been taken off the market
db.execute("""UPDATE listings SET removed = 1
WHERE last_seen < date(?, '-3 days') AND removed = 0""", (today,))
db.commit()
if __name__ == "__main__":
daily_sync()
Run it from cron, a GitHub Action or a serverless scheduler once a day. Listings don’t change by the minute, and a daily cadence keeps your request count predictable.
Mapping to HubSpot, Zoho or Salesforce
The webhook payload maps cleanly onto standard CRM objects:
| Property Finder data | HubSpot | Zoho CRM | Salesforce |
|---|---|---|---|
Listing (property_id) | Custom object or Deal | Custom module | Custom object (e.g. Listing__c) |
Agent (agent_details.id) | Contact | Contact | Contact |
Brokerage (broker_id) | Company | Account | Account |
| DLD area averages | Deal or Company properties | Custom fields | Custom fields |
Store property_id, agent_id and broker_id in unique external ID fields. Then every push is an update-or-create, and re-running the sync never creates duplicates.
Step 5: Enrich new listings with full details
Search results are a summary. For listings that are new to your CRM, call /property-details once to get the description, amenities, RERA permit and brokerage contact details. The response is data.property:
def enrich(property_id):
prop = get("/property-details", property_id=property_id)["data"]["property"]
return {
"description": prop["description"],
"amenities": [a["name"] for a in prop["amenities"]],
"rera_number": prop["rera"]["number"],
"furnished": prop["furnished"],
"broker_name": prop["broker"]["name"],
"agent_name": prop["agent"]["name"],
}
Only enrich new listings. Enriching everything daily multiplies your request count for no gain.
Step 6: Sync agents and brokerages as contacts and companies
/search-agents lists agents active in an area. Filter by category (residential_sale, residential_rent, commercial_sale, commercial_rent) and sort by most_sales or most_rentals to find the most active agents first.
agents = get("/search-agents", location_id=50, category="residential_rent",
sort="most_rentals")["data"]
for a in agents:
contact = {
"external_id": a["id"],
"name": a["name"],
"position": a["position"],
"brokerage": a["broker_name"],
"active_listings": a["total_properties"],
}
# upsert contact in your CRM by external_id
brokers = get("/search-brokers", location_id=50, sort="most_properties")["data"]
for b in brokers:
company = {
"external_id": b["id"],
"name": b["name"],
"address": b["address"],
"active_listings": b["total_properties"],
}
# upsert company in your CRM by external_id
To see an agent’s or brokerage’s current stock, call /agent-properties with agent_id, or /broker-properties with broker_id. That’s useful for a “their listings” tab on a contact or company record.
If you use agent and brokerage contact details for outreach, follow UAE data protection rules and give people a way to opt out.
Step 7: Add DLD market context to deals
When an agent opens a deal in Dubai Marina, the record should say what the area actually rents and sells for. /get-transactions returns DLD-registered transactions with a summary. Note the nested data.data.attributes:
attrs = get("/get-transactions", location_id=50, transaction_type="rented",
period="1y")["data"]["data"]["attributes"]
summary = attrs["summary"]
print(f"New-lease average rent: AED {summary['rent_new_avg_price']:,.0f}")
print(f"Renewal average rent: AED {summary['rent_renew_avg_price']:,.0f}")
print(f"Contracts in period: {attrs['total_items']:,}")
Use transaction_type="sold" for sale prices (sale_avg_price, sale_avg_price_per_sqft). Refresh these weekly, since area averages move slowly.
Planning your request budget
Each API call is one request, whatever it returns. A rough budget for one area:
- Listings:
total_count / 38pages per search. An area with 1,900 rentals is about 50 requests for a full pass. - Details: one request per new listing.
- Agents, brokers, transactions: a handful of requests per area, refreshed weekly.
The free Basic plan includes 700 requests a month. That’s enough to build and test the sync on one or two areas. For daily full syncs across several busy areas, use a paid plan; see pricing. To cut requests further, sort by newest and stop paging once you reach listings you already have. Run a full pass weekly to catch price changes and removals.
Data hygiene tips
- Bedrooms are strings.
"studio"sits alongside"1","2"and so on. Normalise before filtering. - Check
price.period. Rentals can beyearly,monthly,weeklyordaily; annualise before comparing. - Prefer verified listings.
is_verifiedis a useful filter when surfacing listings to clients. - Don’t delete removed listings. Mark them off-market instead; how long a listing stayed up and its price history are valuable later.
Next steps
- For the wider picture of CRM enrichment and lead matching, read Building a Real Estate CRM with UAE Property Data.
- For agent and brokerage directories, see Building an Agent Directory.
- Browse all endpoints in the documentation, or get a free API key on RapidAPI and run the sync above against your own areas.
Ready to Build with UAE Real Estate Data?
Get your API key and start making requests in minutes. Free tier available with 700 requests per month.