Mudah.my Malaysia Listings, Prices & Seller Contacts
Pricing
$2.00 / 1,000 listing record captureds
Mudah.my Malaysia Listings, Prices & Seller Contacts
Returns Mudah.my listings as structured records: title, price, location, postDate, description, imageUrls, sellerName, sellerPhone, category attributes. Covers cars, property, phones, jobs, home goods. Input: searchKeywords or categoryUrls. monitorMode emits price-change and new-listing events.
Pricing
$2.00 / 1,000 listing record captureds
Rating
0.0
(0)
Developer
Farside
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
2 days ago
Last modified
Categories
Share
Track what is for sale on Mudah.my, Malaysia's largest classifieds marketplace — used car listings, property listings, phones, jobs and home goods — with prices, locations, photos, category attributes and the seller contact details Mudah publishes publicly.
What you get
- Every listing as a flat record —
title,price,priceValue,currency,state,area,postDate,description,imageUrls,listingUrl, pluspreviousPricewhen the seller has cut the price. - Category-specific attributes, without a category-specific schema —
make/model/mileage/transmission/engine_capacityon used cars;property_type/rooms/bathroom/size/facilitieson property listings;phone_brand/phone_storage/conditionon phones;salary/contract_type/company_nameon jobs. - Seller contact where Mudah shows it publicly —
sellerPhoneandsellerWhatsappin E.164 form, plussellerName,sellerType,sellerCompanyNameandsellerVerified. Every record carriessellerContactProvenance, so you always know whether a number was published or simply absent. It is never guessed, inferred, or unmasked. - Change monitoring — set
monitorModeand each run diffs against the last one, emitting events for price changes, new listings, delistings, relistings and seller edits. Push them to awebhookUrlor read them from the dataset. This is the part that turns a scrape into a feed. - Graceful degradation with per-field provenance — if Mudah rate-limits a detail page or changes its page structure, the run does not die: it falls back through a documented tier ladder and each field records which tier produced it. You always know whether a null means "the seller did not publish this" or "this run could not see it".
- A run report as the final record — success rate, per-field completeness, which tier served which field, requests per listing, and category and contact-provenance breakdowns, so you can audit a run rather than trust it.
Input
Give it keywords, or give it category page URLs. Everything else is optional.
{"searchKeywords": ["myvi"],"category": "cars","state": "selangor","minPrice": 15000,"maxPrice": 40000,"maxListings": 100}
Or watch a search for changes and get told what moved:
{"categoryUrls": ["https://www.mudah.my/selangor/cars-for-sale"],"maxListings": 50,"monitorMode": true,"watchlistName": "selangor-cars","webhookUrl": "https://example.com/hooks/mudah"}
Or skip discovery entirely and scrape category pages directly:
{"categoryUrls": ["https://www.mudah.my/selangor/cars-for-sale","https://www.mudah.my/penang/properties-for-rent"],"maxListings": 50}
| Field | Type | Default | Notes |
|---|---|---|---|
searchKeywords | array of strings | — | One search per keyword. Provide this or categoryUrls. |
categoryUrls | array of strings | — | Mudah.my category or search URLs. Skips discovery; category, state, minPrice, maxPrice and sortBy are then ignored. |
category | string | all | cars, property-for-sale, property-for-rent, mobile-phones, jobs, home-appliances, motorcycles, and more. |
state | string | malaysia | Any of the 17 Malaysian states and federal territories, e.g. selangor, penang, johor, kuala-lumpur. |
maxListings | integer | 100 | The main driver of run time and cost — one detail page fetch per listing. |
minPrice / maxPrice | integer | — | In Malaysian ringgit (MYR). |
sortBy | string | newest | newest, price_asc, price_desc. |
captureSellerContact | boolean | true | Set false to get listings with no personal data at all. |
monitorMode | boolean | false | Diff this run against the last one and emit change events. |
watchListingIds | array of strings | — | Specific listing IDs or URLs to monitor. Turns on monitorMode. |
watchlistName | string | derived | Names the baseline, so you can keep several independent watchlists. |
webhookUrl | string | — | POST change events here as they are detected. |
proxyConfiguration | object | none | Not needed — see Limitations. |
Output
One record per listing. Abridged, and with the phone number redacted for this README:
{"recordType": "listing","listingId": "114838927","listingUrl": "https://www.mudah.my/2015-perodua-myvi-1-3-ezi-original-paint-1-owner-114838927.htm","title": "2015 Perodua MYVI 1.3 EZi Original Paint 1 Owner","price": "RM 17,800","priceValue": 17800,"currency": "MYR","previousPrice": "RM 18,800","categoryName": "Cars","state": "Selangor","area": "Petaling Jaya","postDate": "Today 17:18","description": "Perodua MYVI 1.3 PREMIUM X FACELIFT …","imageUrls": ["https://cdn.rnudah.com/image_hd/plain/47ffb851…jpg", "… 11 more"],"imageCount": 12,"attributes": {"condition": "Used","make": "Perodua","model": "MyVi","car_type": "Hatchback","transmission": "Auto","engine_capacity": "1298 cc","mileage": "120 000 - 129 999","manufactured_date": "2015"},"sellerName": "Extreme Auto Trader","sellerType": "company","sellerVerified": "verified","sellerPhone": "+6012XXXXXXX","sellerWhatsapp": "+6012XXXXXXX","contactChannels": ["call", "wa"],"sellerContactProvenance": "publicly_shown","capturedAt": "2026-09-01T17:50:57+0800"}
Photos are stored as URLs. Image files are never downloaded into the dataset.
Limitations
Read this part. It is the honest version.
Seller contact is public, but not universal
Mudah.my publishes seller phone numbers on listing pages to anonymous visitors — no login, no "show number" click. This actor reads only what is served that way. It does not unmask, guess, or reconstruct numbers, and it never touches a logged-in session.
But a large minority of sellers choose Mudah's in-app chat instead of publishing a number, and how many depends heavily on the category. Measured over 60 listings across six categories:
| Category | Contact publicly shown |
|---|---|
| Used cars | 100% |
| Property (apartment / condo / house) | 100% |
| Home appliances & kitchen | 100% |
| Jobs | 67% |
| Mobile phones & gadgets | 8% |
| Overall | 78% |
By seller type: company and dealer ads 95%, private ads 52%.
If your use case is dealer or agent lead generation in cars and property, expect near-complete contact coverage. If it is consumer electronics, expect very little — a verification run on 8 Penang iPhone listings returned 1 with a public number and 7 without. Listings with no published number are returned with sellerContactProvenance: "not_available" and a null sellerPhone, never a placeholder.
Completeness depends on what the listing shows
Fields the seller did not fill in do not exist to scrape. A car ad with no mileage stated yields no mileage attribute. Completeness is measured relative to what the live page actually displays, and the measurement uses an independent source: the page's og:/JSON-LD markup, rendered by a different code path than the payload the actor parses. On the acceptance run, 100% of the fields the markup path showed were captured. A record's absent field means the listing lacked it, not that the actor missed it.
Monitoring: the first run has nothing to compare
A watchlist's first run records a baseline and reports no changes — emitting "new listing" for all 50 results of a baseline you never had would be noise, not news. Changes are reported from the second run onward.
Delisting is the part that is easy to get wrong. Mudah's search results are capped and reordered, so a listing missing from today's results has usually just been pushed down by newer ads. Every baseline listing that today's search did not return is therefore re-fetched directly, and only an HTTP 410 counts as delisted — which also means a watchlist keeps tracking its members after they drop out of the search window. A listing that neither resolves nor 410s is reported as unverified rather than guessed at either way.
Webhook delivery is best-effort: a monitoring run will not fail because your receiver is down, and every event is in the dataset regardless. Full behaviour: docs/MONITORING.md.
Degraded runs return less, and say so
Every record carries tier and fieldProvenance. On a healthy run every field
reads T3-detail-full. When a detail page cannot be fetched or parsed, the
record is rebuilt from the search page's own payload and the page's SEO markup
instead, and the affected fields say which tier they came from. Verified by
deliberately breaking each source in turn: all four failure modes still complete
8/8 listings, at the tier the failure implies.
What you lose in a degraded run is specific rather than random: description
is unavailable from search results at all, and seller contact below the full
tier is available only for property listings. Contact provenance then reads
not_observed_at_this_tier rather than not_available — an absence we did not
verify is never reported as one we did. Full ladder: docs/TIERS.md.
Anti-bot is an arms race
Mudah.my sits behind Cloudflare. Today the site serves ordinary HTTP requests without a challenge, and this actor stays deliberately polite — one request at a time and an adaptive delay that backs off when the site rate-limits and recovers when it stops. That keeps success rates high (100% on the runs behind this README) at the cost of speed: roughly 1.2 requests per listing and a run rate that varies with how tolerant the site is that hour.
That balance is a snapshot, not a guarantee. Mudah can tighten its posture at any time, and if it does, throughput drops before correctness does. proxyConfiguration is available but off by default: Mudah currently serves Apify datacenter IPs exactly as it serves Malaysian residential ones — same listings, same prices, same public phone numbers — so a proxy buys nothing and residential proxies would add real cost.
Other things worth knowing
- Pagination has a floor of diminishing returns. A single search reaches roughly 22,000 listings before pages thin out. That is far beyond any realistic
maxListings, but it is not infinite. - Long runs stop on a time budget, never on the platform's kill. The platform ends a run at its timeout (1 hour by default). This actor watches that deadline and stops capturing about two and a half minutes earlier, so the run still ends SUCCEEDED: every captured listing is delivered and billed, the run report explains exactly where the run stopped (
timeBudgetReached: true, with the count still pending), and a rerun with the same input continues from the checkpoint instead of starting over. For larger captures, raise the run timeout in the Console's run settings — or just run again. adTypereflects Mudah's own labelling, which reports"For sale"even on job ads. The value is passed through faithfully rather than corrected.- Prices can be ranges. Job salary bands populate
priceValue(lower bound),priceMaxandpriceIsRange; most other categories leavepriceMaxnull. - A resumed run continues; it does not restart.
maxListingsis a target for the search as a whole, so rerunning an interrupted run finishes the job instead of capturing that many again. On the platform this means resurrecting the interrupted run — every new cloud run starts with a fresh checkpoint store, and rerunning the input there starts over. - Multiple searches split the quota evenly.
searchKeywords: ["myvi", "honda city"]withmaxListings: 10returns five of each, not ten of the first. The same applies to multiplecategoryUrls.
Pricing
Pay-per-event. You are charged for results, not for time:
| Event | Charged when |
|---|---|
result-listing | one per captured listing record |
monitor-change | one per change event emitted in monitorMode |
The run report is not billed — it is run overhead, not a result. Nothing is
charged for a listing that failed to capture, and nothing is charged for a
listing that Mudah reports as gone (HTTP 410); a delisting in monitorMode
bills as the one monitor-change event that tells you about it.
One listing can emit more than one change event: an edited ad can produce
price.changed, title.changed, description.changed, seller.changed,
location.changed, attributes.changed, images.changed and
listing.relisted — up to 8 monitor-change charges for one ad in one
run, plus one result-listing for the recapture. Size maxTotalChargeUsd
with that worst case in mind.
When the charging budget for a run is reached, the actor stops cleanly and says so in the log rather than continuing to work uncharged, and the partial results already pushed are yours.
Handling seller contact data responsibly
Captured phone numbers are personal data of Malaysian individuals and businesses, and Malaysia's Personal Data Protection Act 2010 (PDPA) applies to how you process them.
Publicly visible is not the same as free to use for anything. In particular:
- Use the data for a purpose the seller would reasonably expect — enquiring about the listing they posted.
- The PDPA restricts unsolicited direct marketing, and Malaysia's Communications and Multimedia Act restricts unsolicited commercial messaging. Bulk cold-calling or mass WhatsApp blasts to scraped numbers is where users get into trouble.
- Keep the data no longer than you need it, secure it, and honour any opt-out you receive.
- Set
captureSellerContact: falseif your use case is market and price research. Prices, attributes and locations do not need anyone's phone number.
You are the data controller for anything you collect with this actor. This section is a pointer to the obligations, not legal advice.
Documentation
| Document | What it covers |
|---|---|
| docs/CANARY.md | What Mudah.my actually allows: field matrix per category, URL templates, pagination, the seller-contact verdict, proxy comparison |
| docs/TIERS.md | The capability ladder and per-field provenance — what you still get when the site degrades |
| docs/MONITORING.md | Baselines, the change events, how delisting is verified, webhooks |
| MAINTENANCE.md | What breaks first when Mudah changes, how to detect it, expected upkeep |
Data source and scope
Mudah.my only. No other marketplaces, no third-party enrichment, no email lookup, no data behind a login, and no contact-unmasking services. Everything in the output came from a page an anonymous visitor can load.