# 28Hse Scraper – Hong Kong Property Listings, FSBO & Details (`claude_code_reviewer/28hse-scraper-en`) Actor

Scrape 28Hse, a Hong Kong property portal, without an API or login: sale and rental listings plus full property details — price, saleable area, unit price, layout, district and estate, with agent vs owner (FSBO) marker. Unique owner asking-price data for HK property valuation. Export JSON/CSV/Excel.

- **URL**: https://apify.com/claude\_code\_reviewer/28hse-scraper-en.md
- **Developed by:** [Chad](https://apify.com/claude_code_reviewer) (community)
- **Categories:** Agents, Real estate, Integrations
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 property listings

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/actors/running/actors-in-store.md#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

## 28Hse Scraper – Hong Kong Property Listings & FSBO Details (No API)

Scrape 28Hse.com (Hong Kong's property portal): property listings and full single-property detail, via public HTML endpoints (SSR + JSON-LD) — no login, no token. Structured output, and **FSBO (owner-listed) detection is this Actor's core feature** — a data dimension agency-only sites don't expose, useful for valuation models, proptech, or agency research teams looking to fill out asking-price data.

Complements the same author's `centaline-scraper` (Centaline transaction records): centaline covers completed transactions, this Actor covers active listing detail and FSBO filtering.

### Output fields

#### Property listings (listing mode)

| Field | Type | Description |
|---|---|---|
| property\_id | string | Property ID |
| title | string | null | Title |
| deal\_type | string | null | `buy` or `rent` |
| property\_type | string | null | Property type, e.g. `apartment` |
| price | number | null | Price (HKD). The raw text "售 $830 萬元" is converted to 8300000 |
| saleable\_area | number | null | Saleable area (sqft) |
| gross\_area | number | null | Gross floor area (sqft). **Listing cards don't show gross area, so this field is always null in listing mode** — only detail mode populates it |
| unit\_price | number | null | Unit price per saleable sqft (HKD) |
| district | string | null | District, e.g. "Tsuen Wan" |
| estate | string | null | Estate name |
| rooms | number | null | Room count. Open-plan units (no separate rooms) honestly return null instead of a guess |
| bathrooms | number | null | Bathroom count |
| is\_owner | boolean | **FSBO (owner-listed) detection.** The page marking "業主盤" / "業主自讓" (owner listing) = `true`; agent listing or no marking = `false` |
| agent\_name | string | null | Agency name; for FSBO listings, the owner contact name shown on the page |
| posted\_at | string | null | Posted time (ISO 8601). The raw value is relative ("36 seconds ago", "N days ago"), converted to an absolute timestamp |
| listing\_url | string | null | Full listing URL |
| source\_mode | string | Always `listing` |
| scraped\_at | string | Scrape time (ISO 8601) |

#### Property detail (detail mode, all fields above + the following)

| Field | Type | Description |
|---|---|---|
| address | string | null | Full address |
| floor\_size\_sqft | number | null | Saleable area (sqft), same source as `saleable_area` |
| num\_rooms | number | null | The raw room-count field from the listing page's own JSON-LD. **Note**: this is the site's own JSON-LD structured value, and in testing it doesn't always match the "N 房" (N rooms) shown on the page (the `rooms` field above) — see Notes below. Both fields are kept, each honestly reporting its own source |
| description | string | null | Full listing/estate description |
| agent\_address | string | null | Agency (or owner contact) address |
| agent\_url | string | null | Agency (or owner) page link |
| date\_published | string | null | Posted time (ISO 8601, with timezone) |
| date\_modified | string | null | Last-updated time (ISO 8601, with timezone) |
| image\_count | number | null | Total number of listing images (including floor/unit plans) |

### Modes

| Mode | Input | Output | Billing | Status |
|---|---|---|---|---|
| 📋 listing | dealType + propertyType (or listingUrls) | Property listings | `search-listing` $0.002/item | v1.0 |
| 🏠 detail | propertyIds or propertyUrls | Full single-property fields | `product-detail` $0.008/item | v1.0 |
| 📈 transactions | estateUrls (estate transaction records) | Transaction records | `review-item` $0.003/item | **Not yet available in v1.1** — selecting this mode returns an error |

All charges happen **only after** data is successfully pushed to the dataset. The run's end-of-run log prints `total_items / charged / skipped` so you can reconcile.

### Input fields

| Field | Type | Default | Description |
|---|---|---|---|
| mode | enum | listing | `listing` / `detail` / `transactions` (not yet available, see table above) (required) |
| dealType | enum | buy | `buy` or `rent`. Used by listing mode; also needed in detail mode when using propertyIds (instead of propertyUrls) |
| propertyType | enum | apartment | Property type. Only "Apartment" has been fully tested; other types use the same URL pattern but haven't each been individually verified |
| listingUrls | array | — | Listing mode only, directly specify listing page URLs (overrides dealType/propertyType), multiple allowed, each paginated and counted independently |
| propertyIds | array | — | Detail mode, property IDs (numeric only), needs dealType/propertyType to build the URL |
| propertyUrls | array | — | Detail mode, full property URLs, safest since deal/type info is already in the URL |
| bulkInput | textarea | — | Bulk paste, one item per line, merged into the matching field based on the current mode |
| ownerOnly | boolean | false | Listing mode only: only keep FSBO listings, filtering out agent listings. **FSBO listings are usually rarer** within a single category — if 5 consecutive pages return no matches, the run stops early (see Notes below) |
| maxItems | integer | 50 | Listing mode: max items per listing source; detail mode: max properties to scrape. Maximum **500** |
| dateFrom | string | — | Listing mode only, filters by listing posted time. Format `3d`, `12h`, `2w` (relative) or `MM/DD/YY` (absolute date). When set, paging also stops early, which keeps scheduled monitoring cheaper. Not applicable to detail mode |
| proxyConfiguration | proxy | Apify datacenter proxy | On by default; without a proxy your run shares egress IPs with other runs and gets rate-limited. Turn off only for your own proxy or local testing |

### Input examples

#### 1. Property listings: apartments for sale

```json
{
  "mode": "listing",
  "dealType": "buy",
  "propertyType": "apartment",
  "maxItems": 50
}
```

#### 2. Property detail: a single property

```json
{
  "mode": "detail",
  "propertyUrls": ["https://www.28hse.com/buy/apartment/property-3948689"]
}
```

#### 3. FSBO only, rentals

```json
{
  "mode": "listing",
  "dealType": "rent",
  "propertyType": "apartment",
  "ownerOnly": true,
  "maxItems": 100
}
```

#### 4. Bulk-scrape multiple property IDs

```json
{
  "mode": "detail",
  "dealType": "buy",
  "propertyType": "apartment",
  "bulkInput": "3948689\n3924956\n3990751"
}
```

#### 5. Scheduled monitoring: only listings posted in the last 3 days

```json
{
  "mode": "listing",
  "dealType": "buy",
  "propertyType": "apartment",
  "dateFrom": "3d",
  "maxItems": 200
}
```

### Notes

- Only scrapes public content — no account or token required, uses public HTML endpoints (SSR + JSON-LD), no browser, fast and low-cost
- **Price parsing**: the raw text "售 $830 萬元" ("萬" = 10,000) is automatically converted to a number (× 10,000 = 8,300,000); rental prices usually have no "萬" unit and are taken as-is
- **FSBO (`is_owner`) detection**: only returns `true` when the page explicitly marks a listing as "業主盤" / "業主自讓" (owner-listed). A title that merely mentions "業主" (owner) in passing (e.g. "friend of the owner, price negotiable") is not mistaken for FSBO — that's still an agent listing, and `is_owner` correctly returns `false`
- **`ownerOnly` cost-protection mechanism**: FSBO listings are usually much rarer than agent listings within a single category (e.g. "buy · apartment"). To avoid the crawler paging forever burning cost in a category with no FSBO listings at all, this Actor follows the rule "stop after 5 consecutive requests with no matching data" — **with `ownerOnly` enabled, if 5 consecutive pages have no FSBO listings, the run stops immediately**, even if the category theoretically still has more (agent-only) listings left. If FSBO listings happen to be clustered on later pages, some may be missed as a result; narrower filters (splitting runs by `dealType`/`propertyType`) increase FSBO density and reduce this risk
- **Why `rooms` and `num_rooms` are two separate fields**: `rooms` (shared by listing and detail mode) comes from the "N 房" (N rooms) text shown on the page (reliable, matches what's displayed); `num_rooms` (detail mode only) is the raw room-count field from the site's own JSON-LD structured data. In testing, the two can disagree for the same property (e.g. a listing page displays "3 房" but its JSON-LD `numberOfRooms` is `1`) — this is a discrepancy in 28Hse's own data source, not a parsing error by this Actor. Both fields are kept as-is, each honestly reporting its own source, with no field overriding the other
- Open-plan units (no separate room partitions) honestly return `null` for `rooms` rather than guessing 0 or 1
- `gross_area` (gross floor area) **is only populated in detail mode** — listing cards only show saleable area, not gross area
- Paging stops automatically after 5 consecutive pages with no new data, to avoid burning cost for nothing; if `dateFrom` is set, paging also stops as soon as an entire page of listings is older than `dateFrom` (further reduces cost, listing mode only)
- **Estate transaction records (transactions mode) are not yet available**, planned for v1.1. Selecting this mode currently returns an error message immediately and consumes no charge
- 28Hse currently shows no anti-bot challenge (no CAPTCHA/JS challenge) against the datacenter proxy this Actor uses; if the site tightens defenses in the future, the fallback plan is to switch to a residential proxy — not over-engineered in advance

### FAQ

**Q: Why are `district`/`estate`/`price` null for some listings?**

A: A small number of listing cards have an incomplete layout (e.g. carparks, non-residential listings, or the listing itself is missing data). This Actor honestly returns `null` rather than substituting a guessed value.

**Q: `ownerOnly` returns no FSBO listings at all — is that normal?**

A: Yes. FSBO listings are naturally rarer than agent listings; if 5 consecutive pages have no matches, the run stops early (see Notes above) — this is a deliberate cost-protection mechanism, not a bug. Try a different `dealType`/`propertyType` combination, or check the 28Hse site directly to confirm whether that category currently has any active FSBO listings.

**Q: What's the difference between `propertyIds` and `propertyUrls`?**

A: `propertyUrls` (full URLs) already carry deal type and property type info, making them the safest choice; `propertyIds` (numeric IDs only) require `dealType`/`propertyType` to build the correct URL. The two can also be mixed.

**Q: Why is `gross_area` (gross floor area) always null in listing mode?**

A: 28Hse's listing cards only show saleable area, not gross floor area; gross floor area only appears on the property detail page, so only `detail` mode output has a value. If you need gross floor area, use `detail` mode for the relevant properties.

**Q: When will estate transaction records (Land Registry data) be available?**

A: Planned for v1.1. 28Hse's transaction records go through an AJAX endpoint, and the full set of request parameters hasn't been confirmed yet — this will be added once confirmed, at which point `mode` will simply gain another available option, with no need to switch to a different Actor.

### Disclaimer

This tool is intended for academic research and lawful business analysis only. Users must comply with 28Hse's terms of service and applicable laws. Do not use this tool to infringe on others' privacy, spread false information, or engage in any other unlawful activity. The author is not responsible for any misuse.

***

28Hse scraper, Hong Kong property, HK real estate data, FSBO listings, property listings, HK property prices

# Actor input Schema

## `mode` (type: `string`):

What do you want to scrape? Pick a mode and fill in the matching fields below.

## `dealType` (type: `string`):

Used by listing mode (also needed in detail mode when using propertyIds instead of propertyUrls, to build the URL).

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

Used by listing mode (also needed in detail mode when using propertyIds instead of propertyUrls, to build the URL). Only "Apartment" has been fully tested (GREEN); other types use the same URL pattern but haven't each been individually verified.

## `listingUrls` (type: `array`):

Directly specify 28Hse listing page URLs, e.g. https://www.28hse.com/buy/apartment or https://www.28hse.com/rent/village. Multiple URLs are allowed, each paginated and counted independently. Leave empty to auto-build the URL from "Buy / Rent" + "Property type" above.

## `propertyIds` (type: `array`):

28Hse property IDs (the numeric part of property-{id} in the URL), e.g. 3948689. When used in detail mode, also set "Buy / Rent" and "Property type" so the correct URL can be built.

## `propertyUrls` (type: `array`):

Full 28Hse property URLs, e.g. https://www.28hse.com/buy/apartment/property-3948689. This is the safest way to use detail mode (deal type / property type are already in the URL, unaffected by the fields above).

## `bulkInput` (type: `string`):

**One item per line** (press Enter for a new line, no quotes or commas). Treated as property IDs or property URLs depending on the current mode, and merged into the matching field above at runtime. Fastest way is to copy a whole column from Google Sheets / Excel.

## `ownerOnly` (type: `boolean`):

Listing mode only: only keep listings the page marks as owner-listed / FSBO, filtering out agent listings. FSBO listings are usually rare within a single category — if 5 consecutive pages return no matching listings, the run stops early (see Notes, to avoid burning cost for nothing).

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

Listing mode: max items per listing source. Detail mode: max property IDs/URLs to scrape. Maximum 500.

## `dateFrom` (type: `string`):

Based on the listing's "posted at" time (28Hse listings default to newest-first). Formats: relative 3d (last 3 days), 12h (last 12 hours), 2w (last 2 weeks), or an absolute date MM/DD/YY. Leave empty for no filtering. When set, paging also stops early, which keeps scheduled monitoring cheap. Not applicable to detail mode (a single property has no pages to stop early on).

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

Apify datacenter proxy is on by default: without a proxy your run shares egress IPs with every other run on the platform, making it easy to get rate-limited or blocked. Pages aren't tiny (0.3–1.3MB), but datacenter bandwidth cost is still negligible. Turn it off only for your own proxy or local testing.

## Actor input object example

```json
{
  "mode": "listing",
  "dealType": "buy",
  "propertyType": "apartment",
  "ownerOnly": false,
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `dataset` (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 = {
    "mode": "listing",
    "dealType": "buy",
    "propertyType": "apartment"
};

// Run the Actor and wait for it to finish
const run = await client.actor("claude_code_reviewer/28hse-scraper-en").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 = {
    "mode": "listing",
    "dealType": "buy",
    "propertyType": "apartment",
}

# Run the Actor and wait for it to finish
run = client.actor("claude_code_reviewer/28hse-scraper-en").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 '{
  "mode": "listing",
  "dealType": "buy",
  "propertyType": "apartment"
}' |
apify call claude_code_reviewer/28hse-scraper-en --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,claude_code_reviewer/28hse-scraper-en"
        }
    }
}

```

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/au6hZnBGA5cPrn83z/builds/uaTQporAkj5gP1Q6n/openapi.json
