# Pinhome Property Scraper Indonesia (`produkdigitalali/pinhome-property-scraper-indonesia`) Actor

Scrape Pinhome property listings across Indonesia for sale and rent. Extract prices, price per m², locations, land and building area, bedrooms, bathrooms, agents, developers, financing, and rental terms. Monitor NEW, PRICE\_CHANGED, UPDATED, and safely verified REMOVED listings.

- **URL**: https://apify.com/produkdigitalali/pinhome-property-scraper-indonesia.md
- **Developed by:** [ProdukDigitalAli](https://apify.com/produkdigitalali) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 property results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Pinhome Property Scraper Indonesia

> **Unofficial and independent.** This Actor is not affiliated with, endorsed by, sponsored by, or operated by Pinhome.

### What is this Actor?

Pinhome Property Scraper Indonesia extracts public Pinhome sale and rent listings into structured, analysis-ready property data.

Use it for:

- property aggregation,
- market and price analysis,
- price-per-m² research,
- agent and developer intelligence,
- listing monitoring,
- price-change monitoring,
- dashboards, APIs, scheduled workflows, and AI/data pipelines.

The default input starts from `https://www.pinhome.id/jual/rumah`. You can also provide other public Pinhome search URLs or direct property detail URLs.

### Main output fields

The Actor aims to extract:

- stable `listingId` and canonical `listingUrl`,
- sale/rent transaction type,
- property type and new/secondary listing type,
- exact or range price,
- structured rental `pricePeriod` and `minimumLeaseMonths` when Pinhome publishes them,
- Pinhome price per m² when public,
- transparent calculated price-per-land/building-m² fallbacks for exact-price records,
- location and address,
- bedrooms and bathrooms,
- land/building area,
- floors, certificate, furnishing, electricity, water source, carports,
- facilities and financing options,
- agent/developer information,
- description and public update label,
- monitoring state and price-change fields.

Source values can be missing when Pinhome does not publish them for a listing.

### Quick start

```json
{
  "startUrls": [
    {"url": "https://www.pinhome.id/jual/rumah"}
  ],
  "maxItems": 20,
  "maxPages": 5,
  "concurrency": 8,
  "monitorMode": false,
  "emitChangesOnly": false,
  "detectRemoved": false,
  "maxRemovedChecks": 50,
  "monitorKey": "default",
  "proxyConfiguration": {"useApifyProxy": false},
  "requestTimeoutSeconds": 30,
  "maxRetries": 3
}
```

### Price-per-m² fields

The Actor keeps source and calculated metrics separate:

- `pricePerM2` — preferred usable price-per-m² metric,
- `pricePerM2Source = PINHOME` — Pinhome published the value,
- `pricePerM2Source = CALCULATED_LAND` — calculated from exact price / exact land area,
- `pricePerM2Source = CALCULATED_BUILDING` — calculated from exact price / exact building area,
- `pricePerLandM2Calculated` — explicit land-area calculation,
- `pricePerBuildingM2Calculated` — explicit building-area calculation.

For listings with price or area ranges, the Actor does not invent a single calculated price-per-m² value.

For rental listings, calculated price-per-m² values inherit the listing's rental period. Use `pricePeriod` (`year`, `month`, `week`, or `day`) to interpret the metric. `minimumLeaseMonths` is extracted from Pinhome's public minimum-duration label when available.

### Privacy and public listing data

The Actor is designed for public property metadata. It keeps public agent/developer names when shown on the listing, but direct phone numbers, WhatsApp links, and email addresses are redacted from `description`.

### Monitoring

Enable `monitorMode` to persist state for a repeatable scope.

Change types:

- `NEW` — first observation,
- `PRICE_CHANGED` — exact price or price range changed,
- `UPDATED` — another meaningful property field changed,
- `UNCHANGED` — meaningful data stayed the same,
- `REMOVED` — a missing prior listing was independently confirmed unavailable.

Relative source labels such as `Diperbarui 1 hari lalu` are retained in output but excluded from the meaningful fingerprint, so time passing alone does not create `UPDATED` noise.

With `emitChangesOnly: true`, unchanged rows are suppressed. A successful monitoring run can therefore emit zero dataset items.

A property disappearing from the current search page is **not** enough to mark it removed. The Actor verifies the original detail URL and requires repeated 404/410 confirmation. Network errors, timeouts, 5xx responses, or an inconclusive 200 response never become `REMOVED`.

### Local VS Code testing

This project includes ready-to-run inputs under `test_inputs/` and a PowerShell helper.

Parser smoke test:

```powershell
powershell -ExecutionPolicy Bypass -File scripts/run_local_test.ps1 INPUT_20.json
```

Larger parser test:

```powershell
powershell -ExecutionPolicy Bypass -File scripts/run_local_test.ps1 INPUT_100.json
```

Monitoring sequence — **do not purge storage between runs**:

```powershell
powershell -ExecutionPolicy Bypass -File scripts/run_local_test.ps1 MONITOR_NEW.json
powershell -ExecutionPolicy Bypass -File scripts/run_local_test.ps1 MONITOR_UNCHANGED.json
python scripts/seed_price_change_test.py
powershell -ExecutionPolicy Bypass -File scripts/run_local_test.ps1 MONITOR_PRICE_CHANGED.json
powershell -ExecutionPolicy Bypass -File scripts/run_local_test.ps1 MONITOR_REMOVED.json
```

The price-change seeder modifies only local monitoring state; it does not modify production Actor code or live Pinhome data.

### Expected local monitoring validation

A stable test sequence should look approximately like:

- baseline: `NEW ≈ 30`,
- repeat: `UNCHANGED ≈ 30`, `emittedResults = 0`,
- controlled price test: one `PRICE_CHANGED`,
- removal safety: lowering `maxItems` creates removal candidates, but still-active detail URLs remain in state and are not emitted as `REMOVED`.

Live inventory can change between runs, so a genuine new listing or real price change is not automatically a bug.

### API and schedules

After deployment, the Actor can be run from Apify Console, API, schedules, tasks, and integrations. For recurring monitoring, keep both the same `monitorKey` and comparable `startUrls`.

### Limitations

- Only public Pinhome pages are supported.
- Website markup and URL patterns can change.
- Search pages can change ordering between runs.
- New development pages can represent project-level price/specification ranges rather than one exact physical unit.
- `maxItems` is a ceiling, not a guarantee that a specific query currently exposes that many reachable detail pages.
- Large-scale collection should be validated for reliability and cost before publication pricing is finalized.

### Responsible use

Use this Actor responsibly and only for publicly available information. Users are responsible for ensuring that their collection and use of data complies with applicable laws, contractual obligations, and platform requirements.

**This Actor is unofficial and independent. It is not affiliated with, endorsed by, sponsored by, or operated by Pinhome.**

# Actor input Schema

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

One or more public Pinhome search/listing URLs or direct property detail URLs. Keep the default for a broad house-for-sale smoke test.

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

Maximum number of unique property detail pages selected across all start URLs.

## `maxPages` (type: `integer`):

Maximum numbered search pages fetched per listing URL. Pinhome currently exposes 40 items per broad result page.

## `concurrency` (type: `integer`):

Maximum number of public property-detail requests processed concurrently.

## `monitorMode` (type: `boolean`):

Persist state and classify repeat-run properties as NEW, PRICE\_CHANGED, UPDATED, or UNCHANGED. Confirmed missing detail pages can be emitted as REMOVED.

## `emitChangesOnly` (type: `boolean`):

With monitor mode enabled, suppress UNCHANGED rows and emit only NEW, PRICE\_CHANGED, UPDATED, and REMOVED records.

## `detectRemoved` (type: `boolean`):

Verify previously seen properties that are absent from the current scan. Missing from a search page alone never means REMOVED.

## `maxRemovedChecks` (type: `integer`):

Maximum number of missing prior properties whose detail URLs may be independently verified in one run.

## `monitorKey` (type: `string`):

Optional label that isolates persistent state for separate recurring workflows. Keep the same key and start URLs for comparable runs.

## `proxyConfiguration` (type: `object`):

Optional Apify Proxy or custom proxy settings. Direct connections are the default.

## `requestTimeoutSeconds` (type: `integer`):

Maximum duration of a listing or detail request before retry handling.

## `maxRetries` (type: `integer`):

Retries for temporary network errors, timeouts, and retryable HTTP responses.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.pinhome.id/jual/rumah"
    }
  ],
  "maxItems": 20,
  "maxPages": 50,
  "concurrency": 8,
  "monitorMode": false,
  "emitChangesOnly": false,
  "detectRemoved": false,
  "maxRemovedChecks": 50,
  "monitorKey": "default",
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "requestTimeoutSeconds": 30,
  "maxRetries": 3
}
```

# Actor output Schema

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

Structured public property records. With emitChangesOnly enabled this can be empty when all seen properties are UNCHANGED.

## `summary` (type: `string`):

Discovery count, emitted results, monitoring counts, state sizes, removal-verification diagnostics, and warnings.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.pinhome.id/jual/rumah"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("produkdigitalali/pinhome-property-scraper-indonesia").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 = { "startUrls": [{ "url": "https://www.pinhome.id/jual/rumah" }] }

# Run the Actor and wait for it to finish
run = client.actor("produkdigitalali/pinhome-property-scraper-indonesia").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 '{
  "startUrls": [
    {
      "url": "https://www.pinhome.id/jual/rumah"
    }
  ]
}' |
apify call produkdigitalali/pinhome-property-scraper-indonesia --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,produkdigitalali/pinhome-property-scraper-indonesia"
        }
    }
}

```

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/1GJLWw4OqEpuoGXMv/builds/Hw67cGVK1p587a55o/openapi.json
