# PropertyGuru Scraper \[Only $0.85/1K💰] | SG/MY/TH | PSF | MRT (`ahmed_jasarevic/propertyguru-scraper`) Actor

Scrape property listings from PropertyGuru Group portals — propertyguru.com.sg, propertyguru.com.my and ddproperty.com — with price, PSF, beds/baths, floor and land area, tenure, MRT proximity, agent details and photos. No browser — reads the sites' own JSON. $0.00085 per listing.

- **URL**: https://apify.com/ahmed_jasarevic/propertyguru-scraper.md
- **Developed by:** [Ahmed Jasarevic](https://apify.com/ahmed_jasarevic) (community)
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.83 / 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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Propertyguru Scraper

**Scrape property listings from PropertyGuru Group's Southeast Asia portals — propertyguru.com.sg (Singapore), propertyguru.com.my (Malaysia) and ddproperty.com (Thailand) — with price, PSF, beds/baths, floor and land area, tenure, MRT proximity, agent details, photos, description and coordinates.** Sale and rent listings from all three markets merge into one dataset, at **$0.00085 per listing** — and it works against Cloudflare-protected portals **without a browser**, reading the JSON each site already embeds in its own pages.

Built for SE Asia proptech teams, buyer agents, mortgage brokers, expat relocation services and price-trend researchers.

### Main Use Cases

- **Singapore property data** — build a clean database of SG sale and rental listings with price, PSF, tenure and MRT proximity
- **Thailand real estate data** — Thai listings via ddproperty, with Thai titles and prices returned as correct Unicode
- **Malaysia property listings** — KL-area condos, landed homes and new launches with agent and agency data
- **Property price monitoring** — scheduled runs track price changes, new listings and delistings per district or project
- **Agent lead generation** — collect agent name, agency, CEA licence number and profile URL for outreach
- **HDB resale and condo research** — filter by property type (CONDO, HDB, LANDED, APARTMENT, COMMERCIAL) and district

### How It Works — No Browser, Reads the Sites' Own JSON

PropertyGuru portals are Next.js SSR sites: every page embeds its data as a JSON blob in a `<script id="__NEXT_DATA__">` tag. The actor fetches the page HTML over plain HTTP and parses that embedded JSON directly — no browser, no DOM scraping, no JavaScript execution, roughly **~10x cheaper than a Playwright actor**. A pasted listing URL on the Singapore or Malaysia portal goes one step further and is read from the portal's own JSON listing endpoint — a few KB instead of the **~58 KB detail page, about 9x less residential proxy traffic per row** — with the HTML page kept as an automatic fallback.

The one thing that makes it work: Cloudflare blocks requests that look like plain Node. The actor **impersonates a real browser's TLS and HTTP/2 fingerprint** at the transport layer (`impit`). The measured evidence, from an A/B test against a live MY listing page on the same residential proxy pool:

| Transport | Requests passed |
|---|---|
| Node / Crawlee default TLS signature | **1 of 6** |
| Browser fingerprint impersonation | **6 of 6** |

Every failure under the default signature was a **Cloudflare managed challenge** (`__cf_chl`) — a response to how the request looks on the wire, not a puzzle to solve. No JavaScript was ever executed in either arm.

Both **search pages and single-listing detail pages** are supported. The actor reads the **result pages** — one request per ~20 listings — so a run of any size stays cheap and fast; paste individual listing URLs to read that specific listing. Singapore and Malaysia URLs are fetched from the portal's own **JSON listing endpoint** (a few KB per row), while Thailand and any lookup that fails fall back to the listing's HTML page. Either way you get the detail-only fields (coordinates on every portal, land area, description, full gallery). Blocks are handled by rotating residential IPs (up to 12 attempts per request, paced across a ~7-minute window). Each portal draws from a small pool of exit IPs (three by default) and each **individual IP** is spaced 1s apart by default (`requestDelayMs`, raise it to 2000 for the more conservative pacing), so a run goes several times faster than a single IP would allow without any one IP being hit harder — the same per-IP politeness, just more IPs.

### Build a Southeast Asia Property Database Without the PropertyGuru API

There is no *documented* PropertyGuru listing API. The route widely documented in the wild (`/api/v10/web/search/listings`) belongs to **99.co, a competitor of PropertyGuru — it does not exist on these domains**, and the published mobile-app endpoints are third-party reverse-engineering that existing scrapers only reach *after* launching a browser to clear Cloudflare. What does work is exactly what PropertyGuru serves its own pages: the SSR `__NEXT_DATA__` blob, and — for Singapore and Malaysia listing URLs — the portals' own JSON listing endpoint (`/api/consumer/agent-profile/listing-by-id`), read over plain HTTP with TLS impersonation and no browser at all. That is the cheapest way to get structured property data for Singapore, Malaysia and Thailand — while SG-only competitors on the Store cover just one market.

**Coordinates:** detail pages publish `lat`/`lng` for **all three portals**; the search surface exposes coordinates only for Singapore (map-search). Paste individual listing URLs to get detail-page coordinates everywhere. **Thai text:** ddproperty sends an incorrect charset in `Content-Type`; the actor pins UTF-8 decoding so Thai titles, prices and agent names come back correct (before the fix, prices read as mojibake like `a++ 3,950,000`).

### Input

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `countries` | array | No | `["th"]` | Portals to scrape: Thailand (ddproperty.com), Singapore (propertyguru.com.sg), Malaysia (propertyguru.com.my) — combine several in one run, results merge |
| `startUrls` | array | No | `[]` | Exact URLs to scrape (result pages or single listings, any of the three portals, mixed freely). Overrides `countries` and all filters below |
| `listingType` | enum | No | `sale` | `sale` or `rent` |
| `freetext` | string | No | `""` | Location / district / MRT / project search text, e.g. `Orchard`, `Mont Kiara` |
| `propertyType` | enum | No | `""` | `CONDO`, `HDB`, `LANDED`, `APARTMENT`, `COMMERCIAL` |
| `minPrice` / `maxPrice` | integer | No | `0` | Price range in local currency (SGD / MYR / THB); `0` = no bound |
| `bedrooms` | integer | No | `-1` | Exact bedroom count (`0` = studio, `-1` = any) |
| `useMapSearch` | boolean | No | `false` | Singapore only: return lat/lng for every listing via the map-search surface |
| `maxItems` | integer | No | `50` | Max listings for the whole run (split evenly across selected portals); the run fetches as many pages as needed to reach it |
| `requestDelayMs` | integer | No | `1000` | Gap between requests from the **same exit IP**; each portal uses a small pool of IPs, so lower is faster but blocks sooner |
| `maxBlockRetries` | integer | No | `12` | Retries for a blocked request on a freshly rotated IP |
| `blockRetryBudgetSecs` | integer | No | `420` | Total seconds one blocked page may wait between retries |
| `proxy` | object | No | RESIDENTIAL | Every portal sits behind Cloudflare; each selected portal gets a geo-matched exit IP automatically |

**A pasted URL wins over the Portals dropdown** — a `.com.sg` link scrapes Singapore even if Thailand is selected, and the run logs which portals went unused so the override is never silent. `maxItems` is a total for the run, split evenly between selected portals.

### Output — Property Listing Records for Market Research

One dataset item per listing. **Search-surface rows** include:

| Field | Description |
|---|---|
| `id`, `title`, `url` | Listing identity and direct detail URL |
| `price`, `priceMin`, `priceMax`, `pricePretty`, `currency`, `priceQualifier` | Price in local currency; range bounds for new-build projects; `"Price on Ask"` qualifier when no figure exists |
| `pricePsf` | Price per square foot, when published |
| `floorArea`, `floorAreaValue`, `landArea`, `landAreaValue` | Areas as published and as bare numbers (land on detail pages; key for Thai listings) |
| `bedrooms`, `bathrooms`, `tenure`, `buildYear` | Room counts, freehold/leasehold code (`F`, `L99`), completion year |
| `propertyType`, `propertyTypeCode`, `projectId` | Condo/HDB/Landed/Apartment/Commercial + portal code + project grouping |
| `fullAddress`, `shortAddress`, `districtCode`, `districtText`, `regionText`, `areaText`, `nearestMrt` | Location tiers; Thai-only subdistrict; nearest MRT/BTS with distance (Singapore) |
| `description`, `features`, `photoCount`, `images` | Listing text, amenity tags, photo count and URLs |
| `agentName`, `agentId`, `agentLicense`, `agentProfileUrl`, `agentVerified` | Agent identity, CEA registration, profile link |
| `agencyName`, `agencyId` | Agency identity |
| `latitude`, `longitude` | Coordinates on detail pages for every portal; SG map-search on the search surface |
| `postedOn`, `updatedOn`, `recencyText` | First-posted vs last-changed dates (an edited listing is not "new") |
| `statusCode`, `isProjectListing`, `isPremiumProjectListing`, `projectListingSubType`, `isVerified`, `isOfficialListing`, `isDeveloperListing` | Listing provenance flags. `isProjectListing` comes from the page; Singapore and Malaysia detail rows carry the endpoint's own `isPremiumProjectListing` and `projectListingSubType` instead |
| `sourcePageUrl` | The page the row came from |

### Example Input

```json
{
    "countries": ["sg", "my"],
    "listingType": "sale",
    "propertyType": "CONDO",
    "freetext": "Mont Kiara",
    "minPrice": 500000,
    "maxItems": 200
}
```

Or paste URLs straight from the browser (single listing or a filtered result page, any portal):

```json
{
    "startUrls": [{ "url": "https://www.ddproperty.com/en/property/single-house-in-sriwilai-for-sale-10800011" }],
    "maxItems": 10
}
```

### Example Output — Listing Record (trimmed)

```json
{
    "id": 500249916,
    "title": "The Tapestry",
    "url": "https://www.propertyguru.com.sg/listing/for-sale-the-tapestry-50024991",
    "country": "sg",
    "portal": "propertyguru.com.sg",
    "listingType": "sale",
    "price": 860000,
    "pricePretty": "S$ 860,000",
    "currency": "SGD",
    "pricePsf": "S$ 1,814.35 psf",
    "floorArea": "474 sqft",
    "bedrooms": 1,
    "bathrooms": 1,
    "tenure": "L99",
    "buildYear": 2021,
    "propertyType": "Condominium",
    "fullAddress": "61 Tampines Street 86, Pasir Ris / Tampines",
    "districtText": "Pasir Ris / Tampines",
    "regionText": "Changi / Pasir Ris (D17-18)",
    "nearestMrt": null,
    "description": "High Floor Spacious 1 Bedder Plus Study with a Dual Pool View !",
    "features": ["Pool", "Gym", "Security", "Aircon", "Balcony"],
    "photoCount": 24,
    "agentName": "Alton Nah",
    "agentLicense": "R074064A",
    "agencyName": "PROPNEX REALTY PTE. LTD.",
    "latitude": null,
    "longitude": null,
    "postedOn": "26 Sep 2026",
    "recencyText": "Listed on Sep 26, 2026 (3h ago)",
    "isOfficialListing": true,
    "sourcePageUrl": "https://www.propertyguru.com.sg/property-for-sale?isCommercial=false&listingType=sale&locale=en"
}
```

Detail-page rows (from pasted single-listing URLs) additionally carry `landArea`, `latitude`/`longitude` for every portal, the full image gallery, and both `postedOn` and `updatedOn`. On Singapore and Malaysia these rows are built from the portal's JSON listing endpoint; Thailand — and any Singapore/Malaysia lookup that fails — is built from the HTML page.

### SE Asia Property Listings at $0.00085 Per Listing

The actor bills **$0.00085 per listing** (Apify's `apify-default-dataset-item`) plus the standard **$0.00005 actor start**. Listings read from a **pasted single-listing ("detail") URL** additionally carry a **`detail-listing` event** — charged once per row. Scraping by detail URL costs one request and one result per listing, whereas a **search result page** returns 20-30 listings from a single request, so runs that feed **search, MRT or district URLs** stay at the flat per-result rate and only pasted listing URLs pay the surcharge. The actor uses 256 MB memory, fetches only as many result pages as `maxItems` needs, and **de-duplicates by listing ID** so overlapping result pages never duplicate rows. Proxy cost (RESIDENTIAL, geo-matched) is billed separately by Apify by data transfer; free accounts get a preview, paid plans run to your caps.

### Integrations & Automation

- **API** — run and fetch via the Apify REST API, or call it from AI agents through the Apify MCP server
- **Scheduling** — daily/weekly runs for price-change tracking per district or project
- **Webhooks** — push new listings or price drops to your app, Sheets or Slack
- **Export** — JSON, CSV, Excel, HTML

**Recommended schedule:** daily for price monitoring and new-listing alerts; weekly for full-market database refreshes and research datasets.

### Related Actors

- [PropertyGuru Scraper](https://apify.com/shahidirfan/propertyguru-scraper) — `shahidirfan/propertyguru-scraper` — the most-used SG-focused PropertyGuru actor (120 users, 5.0★)
- [PropertyGuru Singapore Scraper](https://apify.com/abotapi/propertyguru-sg-scraper) — `abotapi/propertyguru-sg-scraper` — Singapore listings
- [PropertyGuru Scraper](https://apify.com/scrapesage/propertyguru-scraper) — `scrapesage/propertyguru-scraper` — PropertyGuru listings
- [PropertyGuru Singapore Scraper](https://apify.com/lentic_clockss/propertyguru-sg-scraper) — `lentic_clockss/propertyguru-sg-scraper` — Singapore listings
- [PropertyGuru Property Scraper](https://apify.com/sian.agency/propertyguru-property-scraper) — `sian.agency/propertyguru-property-scraper` — PropertyGuru listings

This actor is the only one in the set covering **all three portals (SG + MY + TH) in a single run** with a no-browser transport, at $0.00085/listing.

### FAQ

#### Why use this actor instead of the official PropertyGuru API?

There is no documented public PropertyGuru listing API. The `/api/v10/web/search/listings` route often cited in tutorials belongs to 99.co, a PropertyGuru competitor, and does not exist on these domains. This actor reads the same JSON PropertyGuru renders server-side to its own pages — the `__NEXT_DATA__` blob on search and Thai listing pages, and the portals' own listing endpoint for Singapore and Malaysia listing URLs.

#### What are alternatives to this actor for Southeast Asia property data?

SG-focused Store actors: [shahidirfan/propertyguru-scraper](https://apify.com/shahidirfan/propertyguru-scraper), [abotapi/propertyguru-sg-scraper](https://apify.com/abotapi/propertyguru-sg-scraper), [lentic_clockss/propertyguru-sg-scraper](https://apify.com/lentic_clockss/propertyguru-sg-scraper). Off-Store, 99.co operates its own competing portal in Singapore. Ours is the only no-browser actor covering SG, MY and TH.

#### Why is my run returning an empty dataset?

Nearly always Cloudflare blocking — the actor logs the exact cause. The default RESIDENTIAL proxy with geo-matched country is essential (datacenter IPs get challenge pages, not listings). Check `proxy` is on, then wait and re-run: exhausted residential IPs turn over in minutes, and `blockRetryBudgetSecs` (default 420s) rides that out automatically.

#### Why not just use a browser?

A JS challenge is a reaction to how a request looks on the wire, so a browser-accurate TLS/HTTP2 fingerprint satisfies it without executing anything — the A/B above measured 1/6 vs 6/6. A browser also changes the cost profile by ~an order of magnitude (hundreds of MB RAM and seconds per page vs a few KB and a couple of seconds), which defeats the reason to use this actor at all.

#### Why are coordinates `null` in my run?

Coordinates are published on **detail pages for all three portals**; on the search surface only Singapore returns them, and only when you enable map-search. A plain search run returning `null` lat/lng is by design, not missing data — paste individual listing URLs to get detail-page rows with coordinates, land area and the full gallery.

#### Why is a price missing on some listings?

New-build projects are advertised as a range ("RM 1,300,000 - RM 1,500,000") or as "Price on Ask". The range lands in `priceMin`/`priceMax`; "Price on Ask" leaves `price` as `null` (never a misleading `0`) with `priceQualifier` recording the case. `tenure` holds only the freehold/leasehold code (`F`, `L99`), not a full lease-length statement.

#### Can I scrape by district or MRT station?

Yes — `freetext` accepts areas, districts, MRT/BTS stations and project names (e.g. `Orchard`, `Tampines`, `Mont Kiara`); `nearestMrt` comes back for Singapore listings. For complex filters, the most reliable path is building the URL in your browser and pasting it into `startUrls`.

### For AI Agents & LLM Apps

- **Purpose**: given portals, filters or direct URLs, returns one structured row per property listing (price, PSF, rooms, tenure, location tiers, MRT, agent, photos, coordinates on detail pages) from PropertyGuru's SG/MY/TH portals.
- **Minimal working input**:

```json
{ "countries": ["sg"], "listingType": "sale", "maxItems": 50 }
```

- **Variant inputs** — direct URL: `{ "startUrls": [{ "url": "https://www.propertyguru.com.sg/listing/..." }] }`; rent: `{ "countries": ["my"], "listingType": "rent", "freetext": "Mont Kiara", "maxItems": 100 }`.
- **Output fields**: `id, title, url, country, portal, listingType, price, priceMin, priceMax, pricePretty, priceQualifier, currency, pricePsf, floorArea, floorAreaValue, landArea, landAreaValue, bedrooms, bathrooms, tenure, buildYear, propertyType, propertyTypeCode, projectId, fullAddress, shortAddress, districtCode, districtText, regionText, areaText, nearestMrt, description, features, photoCount, images, agentName, agentId, agentLicense, agentProfileUrl, agentAvatar, agentVerified, agencyName, agencyId, latitude, longitude, postedOn, postedUnix, updatedOn, updatedUnix, recencyText, statusCode, isProjectListing, isVerified, isOfficialListing, isDeveloperListing, isPreLaunched, sourcePageUrl`.
- **Behaviors an agent should know**: all three portals sit behind Cloudflare — keep the default RESIDENTIAL proxy with geo-matched countries; datacenter IPs yield empty runs. `startUrls` overrides `countries` and every filter. `maxItems` is split evenly between selected portals. Rows from pasted listing URLs carry land area and lat/lng for every portal; plain search rows return `null` coordinates except SG map-search. `tenure` is a code (`F`, `L99`), not a full statement; `agentLicense` is `null` for Thailand. `price` is `null` (with `priceQualifier: "Price on Ask"`) when no figure exists — do not retry such listings seeking a price. `postedOn` is first-posted; `updatedOn` records edits and must not be treated as a new listing. Thai text is already UTF-8 correct. A run that returns nothing while pages load is Cloudflare — wait and retry, widening `blockRetryBudgetSecs` first.
- **Billing**: pay-per-result — $0.00085 per listing + standard $0.00005 actor start; listings read from pasted single-listing URLs add a `detail-listing` event per row; residential proxy traffic billed separately by Apify.

### Legal & Compliance Disclaimer

This actor is an independent tool and is **not affiliated with, endorsed by, or sponsored by PropertyGuru Group, PropertyGuru Pte Ltd, DDproperty, or any of their affiliates**. It reads only publicly available data that PropertyGuru's portals render server-side to anonymous visitors — it does not log in, bypass paywalls, solve CAPTCHAs, or access private or personal data beyond what the portals publish on listing pages (agent names and agency details are published by the portals themselves). Users are responsible for their own compliance with each portal's Terms of Service, `robots.txt` and applicable law; the actor is intended for legitimate market research, price monitoring and competitive analysis. Scraping publicly available data may nevertheless violate PropertyGuru's Terms of Service — use responsibly and at low request rates.

### SEO Keywords

propertyguru scraper, singapore property data, malaysia property listings, thailand real estate data, ddproperty scraper, sg property for sale, hdb resale data, condo price psf, propertyguru api alternative, southeast asia property, rental market data, property listing database, mrt station property, freehold leasehold data, bangkok property listings, kuala lumpur property, singapore condo rental, property price monitoring, agent lead generation, property market research

# Actor input Schema

## `countries` (type: `array`):

Which PropertyGuru portals to scrape. Pick one or combine several in a single run - each is geo-matched to its own market and the results are merged into one dataset. Pasted URLs below override this entirely, because a URL already states which portal it is.

## `startUrls` (type: `array`):

Optional: one or more PropertyGuru URLs to scrape exactly as they are - result pages or individual listing pages, from any of the three portals, mixed freely in one list. Open the site, filter as you like, and paste the address bar. This overrides the filter fields below, and is the fastest way to re-scrape a page you already know.

## `listingType` (type: `string`):

Sale or rent listings.

## `freetext` (type: `string`):

Optional search text: area, district, MRT station or project name (e.g. 'Orchard', 'Tampines', 'Mont Kiara'). Leave empty for all listings.

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

Optional property type filter (SG/MY).

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

Minimum price in local currency (SGD / MYR / THB). 0 = no minimum.

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

Maximum price in local currency. 0 = no maximum.

## `bedrooms` (type: `integer`):

Exact number of bedrooms (0 = studio). -1 = any.

## `useMapSearch` (type: `boolean`):

Singapore only: also return latitude/longitude for every listing by using the map-search surface. Coordinates on the other portals are only published on individual listing pages, so paste listing URLs to get them.

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

Maximum number of listings for the whole run. When several portals are selected this total is split between them as evenly as possible, so every portal is represented rather than one filling the cap. The actor asks the search for exactly the pages needed to reach this number.

## `maxBlockRetries` (type: `integer`):

How many times a request that comes back blocked may be retried on a fresh IP. Each retry waits longer than the last, spread across a window long enough for the proxy pool to turn over rather than hammering the same exhausted addresses. 0 disables retrying.

## `blockRetryBudgetSecs` (type: `integer`):

Total seconds one blocked request may spend waiting between retries before it is abandoned. The default spreads attempts over ~7 minutes, which is the timescale on which exhausted residential IPs actually turn over. Raise it to ride out a longer block; the container is idle while waiting, so this is far cheaper than any browser-based approach.

## `requestDelayMs` (type: `integer`):

Minimum gap between requests to the same portal. Lower is faster but raises the Cloudflare block rate; 1000 is the current default and 2000 is the more conservative setting. Every run logs how many requests were blocked, so running the same input at two values shows whether the extra speed costs blocks.

## `proxy` (type: `object`):

Select proxies to be used by your crawler. Residential is strongly recommended: every PropertyGuru portal sits behind Cloudflare, which blocks datacenter IPs outright. Each portal selected above is given an exit IP in its own market automatically, unless you pin a country here.

## Actor input object example

```json
{
  "countries": [
    "th"
  ],
  "startUrls": [],
  "listingType": "sale",
  "freetext": "",
  "propertyType": "",
  "minPrice": 0,
  "maxPrice": 0,
  "bedrooms": -1,
  "useMapSearch": false,
  "maxItems": 50,
  "maxBlockRetries": 12,
  "blockRetryBudgetSecs": 420,
  "requestDelayMs": 1000,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "countries": [
        "th"
    ],
    "startUrls": [],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ahmed_jasarevic/propertyguru-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 = {
    "countries": ["th"],
    "startUrls": [],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("ahmed_jasarevic/propertyguru-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 '{
  "countries": [
    "th"
  ],
  "startUrls": [],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call ahmed_jasarevic/propertyguru-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ahmed_jasarevic/propertyguru-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/gJQIw1POjcwt5hfBf/builds/8logn97nsUTkV3RQT/openapi.json
