# Property Finder CRM Integration: Sync Listings, Agents & Brokers into Your CRM

> How to integrate Property Finder data with a real estate CRM in Dubai and the UAE: sync live listings, agents and brokerages into HubSpot, Zoho, Salesforce or your own database, with Python code.

Source: https://uaepropertyapi.com/blog/property-finder-crm-integration
Published: 2026-10-29

"Property Finder CRM integration" can mean two different things, and it is worth separating them before you write any code.

1. **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.
2. **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](/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](/documentation/endpoints/search-rent), [/search-buy](/documentation/endpoints/search-buy), [/search-commercial-rent](/documentation/endpoints/search-commercial-rent), [/search-commercial-buy](/documentation/endpoints/search-commercial-buy)).
- **Full listing details**: description, amenities, RERA permit, agent and brokerage ([/property-details](/documentation/endpoints/property-details)).
- **Agents** in an area and the listings each one holds ([/search-agents](/documentation/endpoints/search-agents), [/agent-properties](/documentation/endpoints/agent-properties)).
- **Brokerages** in an area and their listings ([/search-brokers](/documentation/endpoints/search-brokers), [/broker-properties](/documentation/endpoints/broker-properties)).
- **Off-plan projects** ([/search-new-projects](/documentation/endpoints/search-new-projects)).
- **Market context**: DLD rental and sale transactions and price trends for an area ([/get-transactions](/documentation/endpoints/get-get-transactions), [/price-trend-of-location](/documentation/endpoints/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:

1. A daily job pulls listings for the areas you care about.
2. Each listing is **upserted** into a staging table keyed on `property_id`.
3. 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](/documentation/endpoints/autocomplete-location) and store the IDs. Common ones: Dubai `1`, Dubai Marina `50`, Downtown Dubai `41`, Business Bay `36`, JVC `73`.

```python
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.

```python
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.

```python
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.

```python
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](/documentation/endpoints/property-details) once to get the description, amenities, RERA permit and brokerage contact details. The response is `data.property`:

```python
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](/documentation/endpoints/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.

```python
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](/documentation/endpoints/agent-properties) with `agent_id`, or [/broker-properties](/documentation/endpoints/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](/documentation/endpoints/get-get-transactions) returns DLD-registered transactions with a summary. Note the nested `data.data.attributes`:

```python
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 / 38` pages 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](/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 be `yearly`, `monthly`, `weekly` or `daily`; annualise before comparing.
- **Prefer verified listings.** `is_verified` is 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](/blog/build-real-estate-crm-uae-property-data).
- For agent and brokerage directories, see [Building an Agent Directory](/blog/building-agent-directory).
- Browse all endpoints in the [documentation](/documentation), or [get a free API key on RapidAPI](https://rapidapi.com/happyendpoint/api/uae-real-estate-property) and run the sync above against your own areas.
