# 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
- **Features:** Limited permissions
- **Stats:** 8 total users, 0 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?

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

## Mudah.my Scraper: Cars, Property, Mobiles, Jobs & Classifieds Listings

Mudah.my Scraper turns Mudah.my, Malaysia's classifieds marketplace, into a clean structured data feed. Get listings across cars, motorcycles, property, apartments and condos, computers, mobiles and gadgets, home and furniture, hobbies and collectibles, business equipment, jobs, and services, each with seller and store details, images, timestamps, and category-specific specs (vehicle make/model/mileage, property rooms/size/title type, and more). Search by keyword and filters, or paste listing, category or search links directly, then export to JSON, CSV or Excel, or pull results straight into your app through the API.

### 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, category, or keyword-search links directly.
- **Narrow results your way.** Filter by category, Malaysian state and listing type in search mode; price range also narrows category and search-style pasted links.
- **Category-aware specs extracted automatically.** Vehicle make, model, manufacturing year, mileage, transmission, fuel type, and loan estimates for cars; property type, rooms, bathrooms, size, and title type for property.
- **Rich seller and store data.** Seller name and account type, store id and verification status, seller badges such as Verified Dealer, primary image, image counts, and full listing timestamps.
- **Built for monitoring.** Incremental mode tracks a saved search across scheduled runs and classifies every listing as NEW, UPDATED or UNCHANGED by default (EXPIRED and REAPPEARED need Emit expired), or resume one specific interrupted crawl instead with `resumeFromRunId`.
- **Optional export into your apps.** Send results to Notion, Linear, Airtable, or Apify through MCP connectors.

### Use Cases

- **Car and property buyers:** track new listings matching a price range, state, and category as soon as they're posted.
- **Classifieds resellers:** monitor Mobiles & Gadgets or Home & Furniture listings across every state for pricing and arbitrage opportunities.
- **Market and pricing researchers:** build price datasets across categories to track trends in Malaysia's secondhand market.
- **Real estate and rental platforms:** aggregate property and apartment/condo listings for a comparison or aggregator site.
- **Recruiters and job boards:** pull the Jobs category to cross-post listings or analyze hiring volume by state.
- **Lead generation tools:** watch for new for-sale listings from verified dealers and stores in a specific category.

### 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.name` | `"Second-hand (Used)"` |
| `regionName` / `subarea` | `"Selangor"` / `"Sample Subarea"` |
| `sellerName` | `"Sample Seller"` |
| `sellerVerified` | `true` |
| `seller.storeVerified` / `seller.badges` | `true` / `["Verified Dealer"]` |
| `images.primaryUrl` / `images.count` | image URL / `5` |
| `timestamps.listedAt` / `timestamps.expiresAt` | `"2026-01-01T00:00:00.000Z"` / `"2026-02-01T00:00:00.000Z"` |
| `car.make.name` / `car.year` / `car.mileage` | cars only: `"Sample Make"` / `2020` / `50000` |
| `property.rooms` / `property.size.value` | property only: `"3"` / `1200` |
| `previousPrice` / `discountAmount` / `discountPercent` | the price before a markdown, and the drop (`null` when the seller did not mark the price down) |
| `urgent` / `categorisedBadges` | `true` when the seller flagged the ad URGENT / seller and ad badges, e.g. `{ "seller": [...], "ad": [...] }` |
| `listingId` / `typeId` / `transactionType` | listing id, type code (`s` sell, `u` let) and type name (`sell`, `let`) |
| `description` / `galleryImageUrls` / `details` | Include listing details only: the seller's full text (line breaks kept), every photo URL (high resolution), and a `details` object with `specs`, `videoUrls`, `chatEnabled`, `whatsappEnabled`, `sellerOnline`, `sellerAvatarUrl`, `storeName`, `storeSince`, `companyRoc`, and for property agents `firmSummary`, `firmType`, `estateAgentNumber`, `agentIdentity`, `agentNumber` (each `null` when the page does not show it) |
| `reviews` | `[]` (see note below) |
| `scrapedAt` | `"2026-01-01T00:00:00.000Z"` |
| `changeType` | incremental mode only, e.g. `"NEW"` |
| `changedFields` | incremental mode, `UPDATED` rows only, e.g. `["price", "priceLabel"]` |

Every record also carries: `adId`, `priceAlias`, `categoryId` and `categoryLevel1Name`, `regionId`, the full `seller` object (`userId`, `accountType`, `companyAd`, `storeId`), `images.mediaCount`, the full `timestamps` object (`modifiedAt`, `origDate`, `listTs`), `highlights`, `bundle`, and `raw` (the complete upstream attribute passthrough, so nothing is ever dropped). Car listings also carry `car.transmission`, `car.fuelType`, `car.engineCapacity`, `car.carType`, `car.monthlyPayment`, and `car.carLoan`; property listings also carry `property.type`, `property.bathrooms`, `property.titleType`, and `property.building`. Mudah.my does not operate a seller star-rating or written-review system, so `reviews` is always an empty array; seller trust is instead surfaced through `sellerVerified`, store verification, and seller badges. Incremental mode also adds `firstSeenAt` and `lastSeenAt` to every row.

### How to Use

1. Pick a **mode**: `search` (keyword and filters) or `url` (paste Mudah.my listing, category, or search links).
2. In search mode, add keywords and narrow by category, state, listing type, and price range. In URL mode, paste the links you want scraped. If you leave Start URLs untouched, a URL-mode run scrapes the ten example links (a keyword search within a category, a category page, a state page, an all-categories keyword search, a page-2 category link, a page-2 keyword link, a price-filtered link, a rental page, a long-slug category page and one listing page), so clear or replace them to scrape only your own links. Category pages, state pages (for example `/selangor/cars-for-sale`), rental pages (`/malaysia/properties-for-rent`), keyword links (`?q=`) and page links (`?o=2`, mapped to an approximate start position of about 46 results per page, since the live feed shifts between fetches; the start position is logged) are translated automatically; a link with no recognisable category, state or keyword logs a warning that it scrapes all listings.
3. Set **Max items** (and optionally **Max pages**) to control run size and cost, then click **Start**.
4. Download the dataset as JSON, CSV or Excel, or read it through the API.

**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, no keyword:**

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

**Scrape pasted URLs (a category/search link plus an individual listing link):**

```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
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/mudah-my-scraper").call(run_input={"mode": "search", "queries": ["iphone"], "maxItems": 20})
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(listing["title"], listing["priceLabel"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/mudah-my-scraper').call({ mode: 'search', queries: ['iphone'], maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Max items, Max pages and filters

`maxItems` caps the total listings returned across every search and URL in the run (maximum `100000`; `0` uses that maximum, and the run stops earlier when results are exhausted). When you supply several keywords at once, that budget is split evenly across them so every keyword gets a fair share instead of the first one consuming the whole cap. `maxPages` optionally bounds how many result pages each search or URL walks; leave it empty to walk pages until `maxItems` is reached or a search's results run out, with a hard limit of 500 pages per search or URL.

Category, state and listing type filters apply in search mode only; a pasted search or category link carries its own region, category and type, so those three inputs are ignored for it. Price range narrows both keyword searches and category/search-style pasted links. A pasted link to one specific listing page is looked up directly and returned if found (subject to Max items, Resume and Incremental rules); the category, state, listing type and price filters have no effect on it.

#### Include listing details

Search results alone carry the title, price, photos count, seller and category fields, but not the seller's text or the full photo gallery. Turn on **Include listing details** (`includeDetails`, off by default) and the actor also opens each listing's own page and adds `description`, `galleryImageUrls` and a `details` object (specs table, videos, whether chat and WhatsApp are offered, whether the advertiser is online, store name, and the agent and firm numbers property agents publish). Phone numbers are never collected.

It costs one extra page request per listing, so runs take longer; the number of rows and the billing unit (one result per row returned) do not change. If a listing's page cannot be read, the row is still returned without these three fields and the run log says how many were missed; in incremental mode the values stored from the last run are kept, so a one-off miss does not mark the listing as changed. Switching the option on or off does not start a new incremental baseline and never marks existing listings `UPDATED` in either direction: the first run with it on adds the three fields silently, and a run with it off keeps the values stored by the last run with it on (so turning it on again still compares against them). `whatsappEnabled` is the single WhatsApp contact flag the listing page exposes (`true` when a WhatsApp contact is offered, `null` when the page lists no contacts), and the advertiser's online-now flag (`sellerOnline`) is a live presence indicator that never makes a listing `UPDATED`.

#### Resume and recurring updates

**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 (matched by listing id), returning only the remaining new ones. A checkpoint is also saved automatically as the run progresses, so an Apify platform migration or a **Resurrect** of a failed run picks back up in the *same* run without re-saving or re-charging listings it already collected. No input is needed for the checkpoint; it's automatic.

**Incremental mode** (`incrementalMode`) is for scheduling the same search again and again (for example, daily): the actor remembers the previous run of that search itself, so no run ID needs to be pasted. The first run returns every listing as `NEW`. Later runs classify each listing as `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), or `REAPPEARED`. A listing can only classify `REAPPEARED` if an earlier run had already marked it `EXPIRED`, which only happens when that earlier run had `emitExpired` on and completed a full scan; a listing that has never been marked expired classifies `NEW` or `UPDATED`/`UNCHANGED` instead, never `REAPPEARED`. A listing is `UPDATED` when any scraped field changed (price, title, description, images, seller, highlights, timestamps, raw attributes) and `changedFields` names them; `scrapedAt`, the per-search `rank` and the advertiser online flag `details.sellerOnline` are always excluded, and any path you list in `ignoreFieldsForChanges` is excluded too (it stays in the output). Changing the ignore list never causes a one-time wave of `UPDATED` rows. State is isolated per mode/keyword/category/state/listing type/price/urgent-only filter/URL setup automatically; set `stateKey` to name or deliberately share a monitoring campaign.

Turn on `emitExpired` to also see listings from a previous run that are no longer found, marked `EXPIRED` (and billed). This only happens once a run completes a full, uncapped scan of the tracked search: not when `maxItems` or `maxPages` cut the walk short, not when `resumeFromRunId` 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). An `EXPIRED` row carries the listing exactly as it looked on the last run that still found it.

Combining `incrementalMode` and `resumeFromRunId` is only allowed to bootstrap the very first incremental run from an existing crawl, when no incremental baseline is saved yet for that state key. Once a baseline exists, supplying both fails the run with an explanatory error; remove `resumeFromRunId` or set a different `stateKey`.

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

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each listing, not the full JSON. Each item becomes one entry with a **title** and its key fields (price, category, location, condition, seller, URL) flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (keyword and filters) or `url` (paste links). |
| `queries` | array | no default (prefill: `["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). Search mode only. |
| `state` | string | `all` | Malaysian state to restrict to. Search mode only. |
| `listingType` | string | `all` | `all`, `for-sale` or `for-rent`. |
| `onlyUrgent` | boolean | `false` | Only listings flagged URGENT by the seller. Search mode and category or keyword links in URL mode. |
| `startUrls` | array | ten example links, one per URL shape (search, category, state, all-categories, paged, price-filtered, rental, long-slug category, listing) | URL mode: listing, category, or search pages to scrape. |
| `minPrice` | integer | no default | Minimum price in Ringgit. Applies to searches and category/search-style links, not to a pasted single-listing link. |
| `maxPrice` | integer | no default | Maximum price in Ringgit. Same scope as `minPrice`. |
| `maxItems` | integer | `20` | Maximum listings to return across all searches / URLs. Maximum `100000`; `0` uses that maximum. |
| `maxPages` | integer | no default | Bound on result pages walked per search/URL. Leave empty to walk up to the hard limit of 500 pages. |
| `includeDetails` | boolean | `false` | Also open each listing's own page for the description, the full gallery and seller/contact flags. One extra page request per listing; row count and billing unit unchanged. |
| `resumeFromRunId` | string | no default | Previous run ID or dataset ID; listings already collected there are skipped. |
| `incrementalMode` | boolean | `false` | Daily/recurring monitoring of this same search. |
| `stateKey` | string | no default | Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return (and bill) listings unchanged since the last run, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return (and bill) listings no longer found, marked `EXPIRED`, once a run completes a full scan. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Field paths (dots for nested, e.g. `timestamps.modifiedAt`) that never make a listing `UPDATED`; they stay in the output. |
| `proxy` | object | `{ "useApifyProxy": true }` | Connection configuration. The default is recommended. |
| `mcpConnectors` | array | no default | Optional MCP connectors to also receive a summary of results. |
| `notionParentPageUrl` | string | no default | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### 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": 65000,
  "priceLabel": "RM 65,000",
  "priceAlias": null,
  "currency": "MYR",
  "categoryId": "1000",
  "categoryName": "Cars",
  "categoryLevel1Name": null,
  "category": { "id": "1000", "name": "Cars", "level1Id": null, "level1Name": null },
  "listingType": "sell",
  "condition": { "id": "1", "name": "Second-hand (Used)" },
  "regionId": "8",
  "regionName": "Selangor",
  "location": { "regionId": "8", "region": "Selangor", "subareaId": "301", "subarea": "Sample Subarea", "raw": null },
  "sellerName": "Sample Seller",
  "seller": { "userId": "10000000", "name": "Sample Seller", "accountType": 1, "companyAd": false, "storeId": "100000", "storeVerified": true, "badges": ["Verified Dealer"] },
  "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": 1767225600 },
  "car": { "make": { "id": "10", "name": "Sample Make" }, "model": { "id": "20", "name": "Sample Model" }, "year": 2020, "mileage": 50000, "transmission": "Automatic", "fuelType": "Petrol" },
  "highlights": [],
  "bundle": null,
  "raw": { "...": "the complete upstream attribute passthrough" },
  "reviews": [],
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}
```

#### Dataset views

The dataset has five ready-made views in the Console: **Listings** (core fields for every row, including price drop and urgency), **Changes (incremental)** (change type, changed fields, first/last seen), **Vehicles** (make, model, year, mileage, transmission, fuel, loan eligibility) **Property** (type, rooms, bathrooms, size, title type) and **Listing details** (description, gallery and seller/contact flags, filled when Include listing details is on).

### Plan Requirement

Mudah.my serves its listing data from a search interface that answers from ordinary datacenter connections. With Include listing details on, the actor also reads each listing's own page, which in testing loaded from the same datacenter connection with a browser-grade client (some category pages answer a plain client with a browser challenge). The default connection (`useApifyProxy: true`) is what the actor starts with. A listing page that is refused only moves that one page to the next connection; later pages still start on the default connection. If that connection is refused, the actor tries a residential connection automatically when your plan includes one, and then a backup connection. Apify free plans do not include proxy access or residential proxies, so on a free plan expect the backup connection only: some requests may still fail, and a run that collects nothing ends as FAILED instead of reporting an empty success. For large or frequent runs a paid plan with residential access gives more headroom; pick the group under Connection.

### FAQ

#### How much does it cost?

You pay per listing returned, plus a small one-time run-start charge. The **Pricing** tab shows the current rates. Use **Max items** to cap the cost of any run.

#### Is it legal to scrape Mudah.my?

This actor collects only publicly available classifieds data. You are responsible for how you use it: follow Mudah.my's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Seller-submitted content such as photos and descriptions may carry their own rights.

#### Can I get only new or changed listings on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and updated listings (plus reappeared ones if you also use Emit expired), and unchanged ones are not billed unless you turn on **Emit unchanged**.

#### What's the difference between Resume and Incremental mode?

They solve different problems. **Resume** (`resumeFromRunId`) continues one specific interrupted or oversized crawl so you don't re-scrape and re-pay for listings already saved. **Incremental mode** is for running the *same* search again and again on a schedule and getting only what changed; it remembers state itself, so you never paste a run ID. Use Resume for a one-off continuation, Incremental mode for recurring monitoring.

#### Why did my run fail, or come back with fewer listings than expected?

A run ends as FAILED (never as an empty success) in these cases:

- `resumeFromRunId` is invalid or unreadable, or `incrementalMode` and `resumeFromRunId` are both set while a monitoring baseline already exists for the state key.
- Every list-page request was refused by the connection: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required". Check your plan, or that your own proxy URLs have valid credentials.
- No results page could be loaded for another connection reason: "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later." On a free plan the message says residential proxies are unavailable and the backup connection could not load the pages.

On a free plan the log also shows a warning block "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable."

If some requests fail but others succeed, the run still completes with what it collected. A run that loaded pages but returned zero items logs "No listings were returned this run" and finishes SUCCEEDED: the search matched nothing, or widen the filters. A pasted link that scrapes everything because it has no recognisable category, state or keyword logs a warning. Multi-search runs also save an `OUTPUT` record with per-search counts.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear, or Airtable.

### 🔗 Want more classifieds data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🏷️ <a href="https://apify.com/abotapi/avito-ru-scraper"><b>Avito.ru Scraper</b></a><br>Scrape structured listings from Avito.ru by region, category, filters, or direct URL...</td><td>🏷️ <a href="https://apify.com/abotapi/subito-it-scraper"><b>Subito.it Scraper</b></a><br>Scrape Subito.it listings across cars, real estate, marketplace, jobs, and more. Search...</td></tr>
<tr><td>🏷️ <a href="https://apify.com/abotapi/kijiji-scraper"><b>Kijiji.ca Scraper</b></a><br>Scrape Kijiji.ca listings across property, vehicles, jobs, electronics, furniture...</td><td>🚗 <a href="https://apify.com/abotapi/mobile-de-scraper"><b>mobile.de Scraper</b></a><br>Scrape mobile.de vehicle listings with 30+ fields including price, mileage, registration...</td></tr>
<tr><td>🚗 <a href="https://apify.com/abotapi/willhaben-at-scraper"><b>Willhaben.at Scraper</b></a><br>Scrape Willhaben.at listings into clean JSON with 40+ structured fields, including...</td><td>🚗 <a href="https://apify.com/abotapi/carsales-au-scraper"><b>Carsales.com.au Scraper</b></a><br>Scrape structured vehicle listings from Carsales.com.au from $1 per 1K results. Built to...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/mudah-my-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# 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, only items offered for sale, or only items offered for rent (rentals, rooms, property for rent). Search mode; in URL mode a ...-for-rent page already implies rent.

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

Mudah.my URLs to scrape. Supports individual listing pages (…-<id>.htm), category pages (including state pages such as /selangor/cars-for-sale and rentals such as /malaysia/properties-for-rent), keyword-search pages (?q=) and page links (?o=2, mapped to an approximate start position). Multiple URLs supported. Left untouched, URL mode scrapes the ten example links (one per URL shape).

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

## `onlyUrgent` (type: `boolean`):

Only listings the seller has flagged URGENT (a paid boost on Mudah.my). Applies to search mode and to category/keyword links in URL mode.

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

Maximum number of listings to return across all searches / URLs (up to 100000). Set 0 for the highest allowed limit (100000); the run stops earlier when results are exhausted.

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

Optional bound on result pages walked per search/URL. Leave empty to walk result pages up to a hard limit of 500 pages per search/URL; the run stops earlier at Max items or when a search's results are exhausted.

## `includeDetails` (type: `boolean`):

Also open each listing's own page to add the full description, the whole photo gallery and the seller and contact flags (chat and WhatsApp available, advertiser online, agent and firm numbers where shown). Costs one extra page request per listing, so runs are slower; the number of rows and the price per row stay the same. Phone numbers are never collected. If a page cannot be read, that row is still returned without these fields.

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

Connection configuration. The default Apify connection is recommended.

## `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). A listing is UPDATED when any of its scraped fields changed (price, title, description, images, seller, highlights, timestamps such as modifiedAt, and the raw attributes); the changed names are listed in changedFields. scrapedAt and the per-search rank are always excluded (at any depth) because they change on every fetch, and any field you list in Ignore fields never triggers UPDATED, though it stays in the output. 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/listing type/price/urgent-only filter/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.

## `ignoreFieldsForChanges` (type: `array`):

Field names that must NOT make a listing UPDATED, one per entry. Use dots for nested fields, for example "timestamps.modifiedAt" or "seller.storeId"; array items are covered automatically. Ignored fields stay in the output, they just never count as a change. Only applies when Incremental changes is on.

## `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",
    "https://www.mudah.my/malaysia/cars-for-sale",
    "https://www.mudah.my/selangor/cars-for-sale",
    "https://www.mudah.my/malaysia/all?q=iphone",
    "https://www.mudah.my/malaysia/cars-for-sale?o=2",
    "https://www.mudah.my/malaysia/cars-for-sale?q=honda+civic&o=2",
    "https://www.mudah.my/malaysia/cars-for-sale?q=honda&price=10000-50000",
    "https://www.mudah.my/malaysia/properties-for-rent",
    "https://www.mudah.my/malaysia/mobile-phones-and-gadgets-for-sale",
    "https://www.mudah.my/selangor/honda-civic-for-sale-115524640.htm"
  ],
  "onlyUrgent": false,
  "maxItems": 20,
  "includeDetails": false,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `vehicles` (type: `string`):

No description

## `property` (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",
        "https://www.mudah.my/malaysia/cars-for-sale",
        "https://www.mudah.my/selangor/cars-for-sale",
        "https://www.mudah.my/malaysia/all?q=iphone",
        "https://www.mudah.my/malaysia/cars-for-sale?o=2",
        "https://www.mudah.my/malaysia/cars-for-sale?q=honda+civic&o=2",
        "https://www.mudah.my/malaysia/cars-for-sale?q=honda&price=10000-50000",
        "https://www.mudah.my/malaysia/properties-for-rent",
        "https://www.mudah.my/malaysia/mobile-phones-and-gadgets-for-sale",
        "https://www.mudah.my/selangor/honda-civic-for-sale-115524640.htm"
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// 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",
        "https://www.mudah.my/malaysia/cars-for-sale",
        "https://www.mudah.my/selangor/cars-for-sale",
        "https://www.mudah.my/malaysia/all?q=iphone",
        "https://www.mudah.my/malaysia/cars-for-sale?o=2",
        "https://www.mudah.my/malaysia/cars-for-sale?q=honda+civic&o=2",
        "https://www.mudah.my/malaysia/cars-for-sale?q=honda&price=10000-50000",
        "https://www.mudah.my/malaysia/properties-for-rent",
        "https://www.mudah.my/malaysia/mobile-phones-and-gadgets-for-sale",
        "https://www.mudah.my/selangor/honda-civic-for-sale-115524640.htm",
    ],
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# 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",
    "https://www.mudah.my/malaysia/cars-for-sale",
    "https://www.mudah.my/selangor/cars-for-sale",
    "https://www.mudah.my/malaysia/all?q=iphone",
    "https://www.mudah.my/malaysia/cars-for-sale?o=2",
    "https://www.mudah.my/malaysia/cars-for-sale?q=honda+civic&o=2",
    "https://www.mudah.my/malaysia/cars-for-sale?q=honda&price=10000-50000",
    "https://www.mudah.my/malaysia/properties-for-rent",
    "https://www.mudah.my/malaysia/mobile-phones-and-gadgets-for-sale",
    "https://www.mudah.my/selangor/honda-civic-for-sale-115524640.htm"
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
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/Gu4QFAYgQg2Xe5zad/openapi.json
