# Mudah Cars Scraper (`scrapyx/mudah-cars-scraper`) Actor

Car listings from Mudah.my, Malaysia's largest classifieds: price in RM, make, model, year, mileage band, engine, transmission, fuel, body type, condition, state, seller type, verification badges, images, description. Filter by make/model, state, price, year, mileage, fuel, seller type.

- **URL**: https://apify.com/scrapyx/mudah-cars-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.26 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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 Cars Scraper

Car listings from **Mudah.my**, Malaysia's largest classifieds site (~88,000
live cars): price in RM, make, model, year, mileage band, engine capacity,
transmission, fuel, body type, condition, state and area, dealer or private
seller, verification badges, loan eligibility, every image, and the full
description.

HTTP only. It reads the structured listing cache the site streams with each
page, not the rendered cards, so every field is the site's own value rather
than text parsed off a tile. No login, no key, no browser.

### What it is for

- **Used-car price monitoring** by make/model/state/year band, on a schedule.
- **Dealer inventory tracking** — filter to dealers, group by `sellerName`.
- **Market research** — 39 rows per request, ~10,000 per target.

### Input

| field | what it does |
| --- | --- |
| `makes` | `toyota`, `perodua/myvi`, `mercedes-benz` — the slugs in Mudah's URLs. |
| `regions` | State slugs (`selangor`, `kuala-lumpur`, ...) or `malaysia` (all, default). |
| `searchTerms` | Free text. Loose — see below. |
| `condition`, `priceMin/Max`, `yearMin/Max`, `mileageMax`, `fuelType`, `transmission`, `sellerType`, `mudahCertifiedOnly`, `sortBy` | Filters, every one verified against the rows it returns. |
| `includeFeaturedAds` | Off by default — see below. |
| `maxItems`, `maxConcurrency`, `minRequestInterval`, `proxyConfiguration` | Limits. |

Targets are the product of `regions` × `makes` × `searchTerms`; each gets its
own `SEARCH_SUMMARY` row.

### Four things about this site worth knowing before you trust a run

#### 1. A typo in a make or model returns the whole site, silently

`/malaysia/cars-for-sale/notamake` is HTTP 200 with all 87,695 cars.
`/toyota/notamodel` is all 19,335 Toyotas. No error, no redirect. A scraper
that trusts the URL returns 10,000 cars for "toyata" and every one of them
looks right, because each one is a real car.

This Actor checks page 1: every row's make must match the slug you gave (and
its model must start with the model slug). If it does not, the target stops
with **zero rows** and `stoppedReason: make_not_recognised` /
`model_not_recognised`. Regions are validated locally against the 16 states.

#### 2. Each target stops at about 9,984 rows, and the site pretends that is zero

Pages are 39 cars; page 256 is the last one served. Page 257 answers with an
empty list **and `totalResults: 0`** for a query that has 87,695 — exactly what
a search with no matches looks like. The Actor never requests it, reports
`wallReached: true` and `stoppedReason: wall`, and keeps the real total from
page 1. Narrow by state, make, model or price band to get every row.

#### 3. `searchTerms` is a loose text match

`myvi` → 3,730 results, every one a Myvi. `zzqqxxnotacar` → 58 results: an
Audi TT, a Mazda CX-5, a Perodua Alza. That is a fuzzy matcher finding partial
hits in descriptions, not a fallback catalogue, and there is no honest way to
tell "few real matches" from "noise" per row. So nothing is dropped; the
summary's `keywordHitShare` (share of rows whose title/make/model contains a
query word) tells you what you got: 1.0 on `myvi`, 0.0 on nonsense. For exact
filtering use `makes` and `regions`, which combine with `searchTerms`.

#### 4. Featured ads ignore your search

A browse page carries 6 paid "featured" ads beside its 39 results, and on a
make/model/state page they follow the path (a Myvi page's featured ads are
Myvis). A `?q=` search page carries **39** of them — and a search with zero
results still ships all 39, because they ignore the search text. Off by default;
with `includeFeaturedAds` on, each carries `isFeatured: true` and is never
counted in `carsReturned` or against `totalResults`.

### Other things measured

- **Mileage is a band**, not a reading: `mileageMin: 100000, mileageMax:
  109999`, shown on the site as "100k-110k". There is no exact odometer value.
- **Cloudflare rate-limits at ~20 requests a minute per IP** (HTTP 429, clears
  in ~10 s). Requests are paced (`minRequestInterval`, default 3 s), a 429 is
  retried with a long backoff on a fresh IP, and the proxy default is Apify's
  free datacenter pool so concurrent targets do not share one IP's budget.
  `rateLimitHits` in the summary counts what was absorbed.
- `sort=`, `car_type=`, `search_only=` and `page=` are accepted by the site and
  **ignored**. Only the parameters measured to work are exposed.
- Dealers are ~78% of the listing; `sellerType`, `dealerVerified` and
  `mudahCertified` are on every row.

### Output

- **`CAR`** — `listId`, `url`, `title`, `price` (RM), `make`, `model`, `year`,
  `yearVerified`, `mileageMin`/`mileageMax`/`mileageLabel`, `transmission`,
  `fuelType`, `engineCapacityCc`, `bodyType`, `condition`, `region`,
  `subarea`, `sellerName`, `sellerType`, `dealerVerified`, `mudahCertified`,
  `badges`, `carLoanEligible`, `carLoanTenureYears`, `imageUrl`, `imageUrls`,
  `description`, `postedAt`, `updatedAt`, `expiresAt`, `isFeatured`, `query`,
  `resultPosition`, `pageFound`.
- **`SEARCH_SUMMARY`** — one per target: `totalResults` (the site's count),
  `carsReturned`, `featuredReturned`, `pagesFetched`, `stoppedReason`,
  `wallReached`, `makeFilterVerified`, `modelFilterVerified`,
  `regionFilterVerified`, `keywordHitShare`, `rateLimitHits`, `minPrice`,
  `maxPrice`, and the exact `filters` sent.
- **`ERROR`** — `invalid_input`, `rate_limited`, `page_shape_changed`, and
  anything else that went wrong, with detail.

### Known limits

- Cars for sale only. Mudah's other verticals (property, phones, jobs) use a
  different page contract and are not covered; nor are cars for rent.
- \~9,984 rows per target (see above).
- The listing page carries no phone number, no dealer address and no exact
  mileage; those live on the ad page, which this Actor does not fetch.

# Actor input Schema

## `makes` (type: `array`):

Make slugs as they appear in Mudah URLs - 'toyota', 'perodua', 'mercedes-benz' - or make/model - 'perodua/myvi', 'toyota/vios'. Each becomes its own target. A slug Mudah does not recognise falls open to EVERY car on the site with no error; this Actor checks page 1 and stops such a target with make\_not\_recognised instead of returning 10,000 unrelated cars.

## `regions` (type: `array`):

State slugs: johor, kedah, kelantan, kuala-lumpur, labuan, melaka, negeri-sembilan, pahang, penang, perak, perlis, putrajaya, selangor, sabah, sarawak, terengganu - or 'malaysia' for all (the default). Combined with makes and search terms.

## `searchTerms` (type: `array`):

Optional free-text search (Mudah's ?q=). It is a LOOSE text match: nonsense like 'zzqqxx' still returns a few dozen cars. The summary reports keywordHitShare (rows whose title/make/model contains a query word) so you can see how much of the answer is noise; nothing is dropped. Prefer makes/regions for exact filtering.

## `condition` (type: `string`):

Filter by condition. Verified against the rows returned.

## `priceMin` (type: `integer`):

Lowest price in Malaysian ringgit.

## `priceMax` (type: `integer`):

Highest price in Malaysian ringgit.

## `yearMin` (type: `integer`):

Earliest manufacturing year.

## `yearMax` (type: `integer`):

Latest manufacturing year.

## `mileageMax` (type: `integer`):

Mileage on Mudah is a 5,000-10,000 km BAND, not a reading - rows carry mileageMin/mileageMax. This filters on the band.

## `fuelType` (type: `string`):

Filter by fuel type.

## `transmission` (type: `string`):

Filter by transmission.

## `sellerType` (type: `string`):

Dealers are roughly 78% of the listing. Each row also carries sellerType and dealerVerified.

## `mudahCertifiedOnly` (type: `boolean`):

Only cars carrying the Mudah Certified inspection badge (~700 of 88,000).

## `sortBy` (type: `string`):

Listing order. Matters when a target has more rows than the ~9,984-row wall lets you see.

## `includeFeaturedAds` (type: `boolean`):

Off by default. A browse page carries 6 featured ads beside its 39 results; a search page carries 39 of them - and they IGNORE the search (a zero-result search still ships 39). When on, each such row says isFeatured: true.

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

Overall cap on car rows across every target. Mudah serves 39 cars per page and stops at page 256 - about 9,984 rows per target out of ~88,000 on the site; a target that gets there reports wallReached. Narrow by region/make/model/price to see everything.

## `maxConcurrency` (type: `integer`):

Parallel requests across targets. Cloudflare rate-limits each IP at roughly 20 requests a minute; with the datacenter proxy each target gets its own IP.

## `minRequestInterval` (type: `integer`):

Pacing between request starts, run-wide. Below ~3 seconds a single IP earns HTTP 429 after about 20 requests; the Actor backs off and retries, so a lower value is slower, not faster. Paces starts only - it does not hold a concurrency slot.

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

On by default with Apify's free datacenter pool, because the site's per-IP rate limit is the only constraint and concurrent targets on one IP share one budget. No residential proxy is needed.

## Actor input object example

```json
{
  "makes": [
    "perodua/myvi"
  ],
  "regions": [
    "kuala-lumpur"
  ],
  "condition": "any",
  "fuelType": "any",
  "transmission": "any",
  "sellerType": "any",
  "mudahCertifiedOnly": false,
  "sortBy": "newest",
  "includeFeaturedAds": false,
  "maxItems": 200,
  "maxConcurrency": 2,
  "minRequestInterval": 3,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "makes": [
        "perodua/myvi"
    ],
    "regions": [
        "kuala-lumpur"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/mudah-cars-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 = {
    "makes": ["perodua/myvi"],
    "regions": ["kuala-lumpur"],
}

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/mudah-cars-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 '{
  "makes": [
    "perodua/myvi"
  ],
  "regions": [
    "kuala-lumpur"
  ]
}' |
apify call scrapyx/mudah-cars-scraper --silent --output-dataset

```

## MCP server setup

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