# Mudah.my Cars, Property, Jobs & Marketplace Scraper (`abotapi/mudah-my-scraper`) Actor

Scrape Mudah.my listings across every category: cars, motorcycles, property, mobiles, electronics, home, hobbies, jobs and services. Search by keyword, filter by category, state and price, or paste URLs. Rich records with seller, store verification, images and category-specific specs.

- **URL**: https://apify.com/abotapi/mudah-my-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Jobs, Real estate
- **Stats:** 3 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 listing 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?

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

## Mudah.my Scraper

Collect structured listings from Mudah.my, Malaysia's largest online classifieds marketplace, across every category: cars, motorcycles, property, mobiles and electronics, home and furniture, hobbies and collectibles, jobs, and services. Search by keyword, narrow by category, state and price, or paste listing and category links. Each result is a clean, normalized record with seller and store details, images, timestamps, and category-specific specifications (car make/model/mileage, property rooms/size/title type, and more).

### Why this scraper

- All categories in one Actor: cars, motorcycles, property, apartments and condos, computers, mobiles and gadgets, home and furniture, hobbies and collectibles, business equipment, jobs, and services.
- Two ways to start: keyword and filter search, or paste Mudah.my listing and category URLs.
- Filter by category, Malaysian state, listing type, and price range.
- Forward pagination that walks as many result pages as you allow.
- Rich per-listing output: seller name and account type, store id and verification status, seller badges (for example, Verified Dealer), primary image, image and media counts, and full timestamps.
- Category-aware specifications extracted automatically: vehicle make, model, manufacturing year, mileage, transmission, fuel type, monthly payment and loan estimates for cars; property type, rooms, bathrooms, size, title type, and building for property.
- Optional export of results into your own apps through MCP connectors (Notion, Linear, Airtable, Apify).
- Daily/recurring change monitoring: turn on Incremental mode to get only NEW, UPDATED, and REAPPEARED listings on every scheduled run, or resume one specific interrupted crawl with `resumeFromRunId`.

### Data you get

> Sample shape, values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| id | 100000001 |
| title | Sample Listing Title |
| url | https://www.mudah.my/sample-listing-100000001.htm |
| priceLabel | RM 1,234 |
| price | 1234 |
| currency | MYR |
| categoryName | Cars |
| listingType | sell |
| condition | Second-hand (Used) |
| regionName | Selangor |
| subarea | Sample Subarea |
| sellerName | Sample Seller |
| sellerVerified | true |
| images.primaryUrl | https://img.example.com/images/00/000000000000000000.jpg |
| images.count | 5 |
| timestamps.listedAt | 2026-01-01T00:00:00.000Z |
| car.make.name | Sample Make |
| car.year | 2020 |
| car.mileage | 50000 |
| property.rooms | 3 |
| reviews | \[] |
| changeType | `NEW` (incremental mode only, see "Resume & recurring updates" below) |
| changedFields | `["price", "priceLabel"]` (incremental mode, UPDATED rows only) |
| firstSeenAt | 2026-01-01T00:00:00.000Z (incremental mode only) |
| lastSeenAt | 2026-01-02T00:00:00.000Z (incremental mode only) |

### How to use

Search a keyword across all categories:

```json
{
  "mode": "search",
  "queries": ["iphone"],
  "maxItems": 20
}
```

Search cars in one state within a price range:

```json
{
  "mode": "search",
  "queries": ["honda civic"],
  "category": "cars",
  "state": "selangor",
  "listingType": "for-sale",
  "minPrice": 50000,
  "maxPrice": 150000,
  "maxItems": 50
}
```

Browse a whole category by state:

```json
{
  "mode": "search",
  "category": "property",
  "state": "kuala-lumpur",
  "maxItems": 100
}
```

Scrape pasted URLs (listing pages, category pages, or keyword-search pages):

```json
{
  "mode": "url",
  "startUrls": [
    "https://www.mudah.my/malaysia/cars-for-sale?q=honda+civic",
    "https://www.mudah.my/sample-listing-100000001.htm"
  ],
  "maxItems": 30
}
```

### Input parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| mode | string | search | search (keyword and filters) or url (paste links). |
| queries | array | \["iphone"] | Keywords to search. Leave empty to browse a whole category or state. |
| category | string | all | Category to restrict to (cars, property, mobiles-gadgets, jobs, and more). |
| state | string | all | Malaysian state to restrict to. |
| listingType | string | all | all, or for-sale only. |
| minPrice | integer | | Minimum price in Ringgit. |
| maxPrice | integer | | Maximum price in Ringgit. |
| startUrls | array | | URL mode: listing, category, or search pages to scrape. |
| maxItems | integer | 20 | Maximum listings to return across all searches / URLs. Set 0 for unlimited. |
| maxPages | integer | | Bound on result pages walked per search/URL. Leave empty to walk every result page; the run then stops at Max items or when a search's results are exhausted. |
| resumeFromRunId | string | | Previous run ID or dataset ID. Listings already collected there (matched by listing id) are skipped, so the run only saves new ones. For recurring daily/weekly monitoring of the same search, use `incrementalMode` instead, see "Resume & recurring updates" below. |
| incrementalMode | boolean | false | Daily/recurring monitoring of this same search. First run returns everything as `NEW`; later runs return only `NEW`/`UPDATED`/`REAPPEARED` by default. See "Resume & recurring updates" below. |
| stateKey | string | | Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. Auto-derived from your mode/keyword/category/state/price/URL setup when left empty. |
| emitUnchanged | boolean | false | Incremental mode only. Also return listings unchanged since the last run, marked `UNCHANGED`. Adds and bills extra rows you already have. |
| emitExpired | boolean | false | Incremental mode only. Also return listings from a previous run no longer found, marked `EXPIRED`, once a run has fully scanned the tracked search (not capped, not a resume, and not a URL-mode run that only looked up individual listing URLs). Adds and bills extra synthetic rows. |
| proxy | object | Apify | Connection configuration. The default is recommended. |
| mcpConnectors | array | | Optional MCP connectors to also receive results. |

#### Resume & recurring updates

There are two different things here, pick the one that matches what you're doing:

| Need | Use |
| --- | --- |
| A crawl stopped and should continue | `resumeFromRunId` / automatic checkpoint recovery |
| Run the same search every day and receive only changes | `incrementalMode` |
| Keep separate daily campaigns for similar searches | distinct `stateKey` values |
| Run a normal full snapshot | leave both off |

**Resume** (`resumeFromRunId`) continues one specific interrupted or previous large crawl: paste a run ID or dataset ID and this run skips listings already collected there, returning only the remaining new ones. A **checkpoint** is also saved automatically as the run progresses: if the run is interrupted by an Apify platform migration or you click **Resurrect** on a failed run, it picks back up from where it left off in the *same* run, without re-saving (or re-charging for) listings it already collected. No input is needed for the checkpoint; it's automatic.

**Incremental mode** (`incrementalMode`) is for a schedule (for example, daily): the actor remembers the previous run of the *same* search by itself, so you never paste a run ID. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` listings by default, duplicates and unchanged listings are suppressed (and not charged). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned (and billed for). State is isolated per mode/keyword/category/state/listing type/price/URL setup automatically; set `stateKey` to name or deliberately share a monitoring campaign.

Combining both: setting `incrementalMode` and `resumeFromRunId` together is only allowed to **bootstrap** the very first incremental run from an existing crawl (no incremental state saved yet for this search). Once a baseline exists, supplying both fails fast with an explanatory error, remove `resumeFromRunId` or set a different `stateKey`.

Scheduled-run example, same search, run daily:

Day 1 (first run ever for this search):

```json
{ "mode": "search", "queries": ["iphone"], "incrementalMode": true }
```

→ every listing comes back with `"changeType": "NEW"`.

Day 2 (the schedule fires again, identical input):

```json
{ "mode": "search", "queries": ["iphone"], "incrementalMode": true }
```

→ listings whose price/title/seller/etc. changed come back as `"changeType": "UPDATED"` with `changedFields` listing what changed, brand-new listings come back as `"changeType": "NEW"`, listings that vanished and came back come back as `"changeType": "REAPPEARED"`, and listings that are still there unchanged are **not** returned at all (suppressed, not charged) unless `emitUnchanged` is on. Turn on `emitExpired` to also see listings that disappeared since the last run, marked `EXPIRED`, once a run completes a full scan (not capped by Max items/Max pages, not a resume, and not a URL-mode run that only looked up individual listing URLs, since such a run never walks the tracked search and so cannot prove anything is gone). An `EXPIRED` row carries the listing exactly as it looked on the last run that still found it, `lastSeenAt` says when that was.

### Output example

> Sample shape, values are illustrative placeholders, not from a live listing.

```json
{
  "id": "100000001",
  "adId": "100000002",
  "url": "https://www.mudah.my/sample-listing-100000001.htm",
  "title": "Sample Listing Title",
  "price": 1234,
  "priceLabel": "RM 1,234",
  "currency": "MYR",
  "category": { "id": "1020", "name": "Cars", "level1Id": null, "level1Name": null },
  "listingType": "sell",
  "condition": { "id": "1", "name": "Second-hand (Used)" },
  "location": { "regionId": "8", "region": "Selangor", "subareaId": "301", "subarea": "Sample Subarea", "raw": null },
  "seller": { "userId": "10000000", "name": "Sample Seller", "accountType": 1, "companyAd": false, "storeId": "100000", "storeVerified": true, "badges": [] },
  "sellerVerified": true,
  "images": { "primaryUrl": "https://img.example.com/images/00/000000000000000000.jpg", "count": 5, "mediaCount": 5 },
  "timestamps": { "listedAt": "2026-01-01T00:00:00.000Z", "modifiedAt": "2026-01-01T00:00:00.000Z", "origDate": "2026-01-01T00:00:00.000Z", "expiresAt": "2026-02-01T00:00:00.000Z", "listTs": 1700000000 },
  "car": { "make": { "id": "10", "name": "Sample Make" }, "model": { "id": "20", "name": "Sample Model" }, "year": 2020, "mileage": 50000, "transmission": "Automatic", "fuelType": "Petrol" },
  "reviews": [],
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}
```

### Send results into your apps (MCP connectors)

You can optionally pipe each run's results into the apps you already use through Model Context Protocol (MCP) connectors. Authorize a connector under Apify, Settings, API and Integrations, then select it in the `mcpConnectors` input. For Notion, also set `notionParentPageUrl` and each item is created as a page under it; other connectors receive a best-effort write. The connector receives a condensed, human-readable summary per item (a title plus the key fields), while the complete record always stays in the Apify dataset. Leave `mcpConnectors` empty to skip; it never changes the dataset output.

### Notes on coverage

- Seller ratings and reviews: Mudah.my does not operate a seller star-rating or written-review system, so the `reviews` field is always an empty array. Seller trust is instead surfaced through the `sellerVerified` flag, the store verification status, and seller badges such as Verified Dealer.
- Contact details, full photo galleries, and long descriptions are shown only on individual protected listing pages and are not included; each record carries the primary image plus the full structured card data (price, specifications, seller, store, category, location, and timestamps).
- Property for-rent listings are browsed through their own category and URL rather than the for-sale listing type.

### Requirements

Runs on any Apify account with the default connection settings. No extra configuration is required for typical use.

# Actor input Schema

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

How to start: run a keyword/category search, or scrape specific pasted URLs.

## `queries` (type: `array`):

One or more keywords to search across Mudah.my (e.g. iphone, honda civic, condo). Leave empty to browse a whole category or state.

## `category` (type: `string`):

Restrict results to a category. Choose All to search across every category.

## `state` (type: `string`):

Restrict results to a Malaysian state. Choose All for the whole country.

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

All listings, or only items offered for sale. (Property for-rent is browsed via its own category and URL.)

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

Mudah.my URLs to scrape. Supports individual listing pages (…-<id>.htm), category pages, and keyword-search pages. Multiple URLs supported.

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

Only listings priced at or above this amount in Malaysian Ringgit.

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

Only listings priced at or below this amount in Malaysian Ringgit.

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

Maximum number of listings to return across all searches / URLs. Set 0 for unlimited (the run then stops when results are exhausted).

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

Optional bound on result pages walked per search/URL. Leave empty to walk every result page; the run then stops at Max items or when a search's results are exhausted.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a full-catalogue walk across separate runs. Listings already collected there (matched by listing id) are skipped, so this run only saves the new ones. For recurring daily/weekly monitoring of the same search, use Incremental mode below instead. Leave empty for a normal fresh run.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching listings as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED listings, unchanged ones are suppressed (and not billed). Turn on "Emit unchanged" or "Emit expired" only when you also want those listings returned (and billed). State is kept separately for each mode/keyword/category/state/price/URL setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the mode/keyword/category/state/price/URL setup, different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return listings that have not changed since the last run, marked UNCHANGED. This returns, and bills, extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return listings that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search, so not when Max items/Max pages capped it, not when Resume was used, and not on a URL-mode run that only looked up individual listing URLs (such a run never walks the tracked search, so it cannot prove anything is gone). This returns, and bills, extra synthetic rows, so leave it off unless you need expiry tracking.

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

Connection configuration. The default Apify connection is recommended.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a page-per-item export; other connectors get a best-effort write. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "iphone"
  ],
  "category": "all",
  "state": "all",
  "listingType": "all",
  "startUrls": [
    "https://www.mudah.my/malaysia/cars-for-sale?q=honda+civic"
  ],
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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": "search",
    "queries": [
        "iphone"
    ],
    "startUrls": [
        "https://www.mudah.my/malaysia/cars-for-sale?q=honda+civic"
    ],
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/mudah-my-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 = {
    "mode": "search",
    "queries": ["iphone"],
    "startUrls": ["https://www.mudah.my/malaysia/cars-for-sale?q=honda+civic"],
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/mudah-my-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 '{
  "mode": "search",
  "queries": [
    "iphone"
  ],
  "startUrls": [
    "https://www.mudah.my/malaysia/cars-for-sale?q=honda+civic"
  ],
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/mudah-my-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/mudah-my-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/bWhNkj72bMaD5h9Bv/builds/eZFHUFZxGEHaGQrPB/openapi.json
