Israel Real Estate Scraper - Yad2 & Madlan Listings
Pricing
$3.00 / 1,000 listings
Israel Real Estate Scraper - Yad2 & Madlan Listings
Scrape Israeli apartments, houses and commercial property from Yad2 and Madlan in one run. One row per apartment, price history, price drops, neighborhood scores and new-listings-only monitoring.
Pricing
$3.00 / 1,000 listings
Rating
0.0
(0)
Developer
Lead Proof
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
1
Monthly active users
0.1 hours
Issues response
3 days ago
Last modified
Categories
Share
Search Israel's biggest property sites, Yad2 and Madlan, plus Homeless if you want it, in one run. Get apartments, houses and commercial property for rent or sale as one clean dataset: one row per apartment even when it is listed on both sites, with prices, full addresses and coordinates, price history, price drops and neighborhood scores. Export JSON, CSV or Excel, or connect the Actor to the Apify API, n8n, Make or Zapier.
Why this Actor
- Two sites, one list. Yad2 and Madlan carry different listings. Searching both finds more of the market, and the same apartment on both sites is delivered once and charged once.
- More than a listing. Madlan rows add every price change with its date and neighborhood scores for schools, parks, quiet streets and transport. Yad2 rows show the previous price on a price drop.
- Fast and light. Searches run in parallel from Israeli residential IPs and read the compact data the sites use themselves, so thousands of listings arrive in about a minute at low platform cost.
- Homeless too. Add
homelessto Sources to include homeless.co.il boards (private rentals, broker rentals, sales and commercial). Homeless pages are read in a real Chrome window, one fresh session per page, so it is slower than the other two sites; give such runs at least 1024 MB of memory. - Monitoring built in. Turn on Only new or changed listings and schedule the Actor: each run delivers just the new listings and price changes since the last one.
Quick start
- Choose Transaction: Rent, Sale or Commercial.
- Keep both Sources or pick one site.
- Fill the Filters you need, such as city, price and rooms, and set Maximum results.
- Run, then download the dataset or read it through the API.
3-room apartments for rent in Tel Aviv
{"transactionType": "rent","sources": ["yad2", "madlan"],"filters": {"city": "Tel Aviv", "rooms": 3, "maxPrice": 8000},"maxResults": 1000}
Price drops in Haifa, 4 rooms or more
{"transactionType": "sale","filters": {"city": "חיפה", "minRooms": 4, "priceDropped": true},"maxResults": 1000}
Daily watch on several moshavim
{"transactionType": "sale","filters": {"city": "צפרירים, אביעזר, נס הרים, גפן"},"maxResults": 500,"onlyNewListings": true,"monitorName": "ella-valley"}
Filters
| Filter | Yad2 | Madlan |
|---|---|---|
| City or locality (several, comma separated) | Applied by the site | Applied by the site |
| Price, rooms, size in sqm | Applied by the site | Applied by the site |
| Property type | Apartment applied by the site, others checked on each row | Applied by the site |
| Floor | Checked on each row | Applied by the site |
| Parking, elevator | Needs Read listing details | Applied by the site |
| Price dropped | Checked on each row | Applied by the site |
| Area (Yad2 grouping) | Applied by the site | Not available |
Homeless applies city, price and rooms itself; the other filters are checked on each row. Homeless rows carry the board's columns: property type, city, neighborhood, street and number, rooms, floor, price, entry date and last update.
Every filter is also checked on every returned row, so a row that does not match is never delivered. City names are the Hebrew names the sites show; common English names such as Tel Aviv, Jerusalem and Haifa work too, and small localities work.
Output
Each row has the same fields whichever site it came from:
| Field | Description |
|---|---|
id, url, source | Listing ID, listing URL, and yad2, madlan or homeless |
transactionType | rent, sale or commercial |
title, description | Property type, street and city; description with details (Madlan commercial rows include it) |
price, currency, previousPrice | Price in ILS (null when shown as "price on request") and the earlier price after a drop |
location | Street and number, neighborhood and city |
images | Public image URLs |
seller | private or agency, with agency name and licence when public |
attributes | City, area, neighborhood, street, house number, latitude and longitude, floor, total floors, property type, rooms, size, condition, parking, elevator, amenities, entry date |
publishedAt, updatedAt | Listing dates |
changeType | Monitoring mode: new or price_changed |
Madlan rows add attributes.priceHistory (every listed price with its date), attributes.neighborhoodScores and attributes.buildingYear. Values a site does not show stay null. Private sellers' names and phone numbers are not collected.
One row per apartment
With One row per apartment on (the default), a Madlan listing and a Yad2 listing with the same city, street, house number, rooms and floor, and prices within 5% of each other, count as one apartment: the first one found is delivered and the other is skipped and not charged. Listings without a street number are never merged. SUMMARY reports how many duplicates were skipped. Turn the option off to receive every site's listing.
Monitoring new listings
With Only new or changed listings on, the Actor remembers each delivered listing and its price in a named key-value store in your account. The next run with the same search delivers only listings it has not delivered before, plus listings whose price changed, each marked in changeType. Use Monitor name to keep several watches apart.
Prepare a monitor in the Actor UI
No local Python installation is needed for the built-in preparation workflow:
- Set Operation to Preview monitor from export. Choose the intended transaction (Rent, Sale or Commercial) and enter a Monitor name prefix.
- Under Prepare a monitor from an existing export, upload a UTF-8 CSV/JSON file to your Apify storage, or paste its contents. Set the ID and price column names if they differ from
idandprice. - Start the preview and open Monitor preview in Output. Review the unique-ID count, duplicate count, null prices, sample rows and errors. Invalid rows or conflicting prices prevent creation; no partial import is written.
- Keep the same export, column names, transaction and name prefix. Change Operation to Create new monitor from reviewed export and paste
importFingerprintfrom the successful preview into Preview confirmation code. Start this preparation run. - Open Run summary. It returns the newly created
monitorName,monitorKeyandcollectionInput. Creation adds a unique suffix to your prefix, so separate creation runs never target the same monitor. Your existing rental monitor and datasets remain untouched. Resurrecting a preparation run preserves any already-written destination, even if later collection has changed it.
Preparation only reads the supplied export and uses Apify storage. It never contacts Yad2, Madlan or Homeless, creates a proxy, pushes dataset rows or triggers listing charges. It does not start collection automatically. The file upload itself uses Apify storage; this is an Actor preparation run, not the fully offline utility below. Maximum input: 10 MiB and 100,000 rows. Files must be uploaded to the same account running the Actor; remote file URLs are rejected.
Same-price duplicates are consolidated; conflicting prices, duplicate JSON keys, malformed CSV rows, non-Yad2 source rows and rows explicitly declaring a different transaction are rejected. Blank/null/zero prices become null; IDs preserve case. Only the first 100 errors and first 10 normalized rows appear in the preview. The confirmation code binds the normalized prices, IDs, column names, transaction and name prefix. Creating another monitor deliberately requires another preparation run and produces another unique name.
The returned collectionInput is a starting point for a later, separately authorized collection. Add your filters and result limits and retain the exact returned monitor name. Current collection still rotates sessions and retries blocked requests, so it is unsuitable if those behaviors are prohibited. Monitor preparation does not solve pagination gaps or provide licensed source access.
Preserve an existing monitor and seed a separate one
Monitor records live in the named key-value store yad2-all-monitor in your Apify account. A record is a JSON object mapping a listing ID to its last delivered price, for example:
{"abc12xyz": 2450000, "def34uvw": null}
For Yad2 real estate, the ID is the case-sensitive token in /realestate/item/<token>, without a URL, prefix or whitespace. Use numeric whole ILS prices, not numeric strings; use null for unknown or on-request prices (source price 0 maps to null). Seed prices are compared directly with current feed prices. An unchanged seeded ID is skipped before details and delivery. A new ID or different price is eligible for delivery and the normal listing charge. Importing a record itself delivers no rows and incurs no listing charge.
Choose a new, unused Monitor name for sales, distinct from the rental monitor. Named keys are calculated as follows, after trimming the name as the Actor does:
import hashlib, rename = "tlv-sales".strip()slug = re.sub(r"[^a-zA-Z0-9-]+", "-", name).strip("-")[:40]digest = hashlib.sha256(name.encode()).hexdigest()[:12]key = f"MONITOR-{slug + '-' if slug else ''}{digest}"# MONITOR-tlv-sales-fc98dca68b20
A named key does not depend on filters, transaction or source: use distinct names for distinct watches. Without a name, the key is MONITOR- plus the first 24 hex characters of SHA-256 of Python json.dumps(semantic, sort_keys=True, ensure_ascii=False).encode(). semantic contains the parsed category, transaction, vehicle, filters and urls; it also includes the ordered sources list unless that list is exactly ["yad2"]. Limits and detail settings are not part of this key. For imports, use a name instead of reconstructing an unnamed scope.
To prepare an authorized Yad2 export locally, save the Python utility below as prepare_yad2_monitor.py, then run it on your computer. It requires only Python's standard library, no API credentials or Actor run:
python prepare_yad2_monitor.py sales.csv --monitor-name tlv-sales --output sales-monitor.json
It accepts CSV or JSON rows with id and price, or a JSON ID-to-price object. For different column names, use --id-field listingId --price-field currentPrice. The ID field may contain a Yad2 real-estate URL; the utility extracts its token, trims whitespace and preserves case. Numeric strings become numbers; blank, null and zero prices become null. Missing price fields, negative/fractional prices, formatted prices such as 2,450,000, other-source rows and conflicting duplicate IDs are rejected for manual reconciliation. The utility refuses to overwrite an existing local output file. It does not upload records, contact source sites, start runs or charge for listings.
Upload the output JSON as a record in yad2-all-monitor using the printed key and content type application/json. Check that the target key is absent before creating it; do not overwrite an existing monitor. Verify the stored record before scheduling any collection. Use sources: ["yad2"], transactionType: "sale" and the printed monitorName when a subsequent collection is appropriate. A stale export can generate legitimate price-change charges.
If the existing rental monitor must remain byte-for-byte unchanged, download a backup and copy its record to another unused named key, then use only that new name for any approved future work. Leave the original dataset and key untouched. Running against the original monitor updates its contents. Do not run concurrent collectors against the same monitor: persistence replaces a record snapshot and is not a transactional merge. Dataset delivery and monitor persistence are separate operations, so hard interruptions can leave a gap; cross-run exactly-once billing is not guaranteed. Reconcile delivered IDs from the retained dataset into a separate monitor before recovery after such an interruption.
Offline preparation utility
"""Prepare a Yad2 real-estate monitor record offline; never access Apify or a source site."""import argparseimport csvimport jsonimport refrom decimal import Decimal, InvalidOperationfrom pathlib import Pathfrom urllib.parse import urlsplitimport hashlibdef monitor_key(config, name):slug = re.sub(r'[^a-zA-Z0-9-]+', '-', name).strip('-')[:40]digest = hashlib.sha256(name.strip().encode()).hexdigest()[:12]return f'MONITOR-{slug + "-" if slug else ""}{digest}'def listing_id(value):if not isinstance(value, str):raise ValueError('Listing ID must be a Yad2 token or listing URL string')value = value.strip()if '://' in value:url = urlsplit(value)if (url.scheme not in {'http', 'https'} or url.netloc not in {'yad2.co.il', 'www.yad2.co.il'}or not re.fullmatch(r'/realestate/item/[a-zA-Z0-9_-]{4,40}/?', url.path)):raise ValueError('Expected a Yad2 real-estate listing URL')value = url.path.rstrip('/').rsplit('/', 1)[-1]if not re.fullmatch(r'[a-zA-Z0-9_-]{4,40}', value):raise ValueError('Invalid Yad2 listing token; prefixes are not accepted')return valuedef ils_price(value):if value is None or isinstance(value, str) and value.strip().lower() in {'', 'null'}:return Noneif isinstance(value, bool) or not isinstance(value, (str, int, float)):raise ValueError('Price must be a whole ILS number or null')try:price = Decimal(str(value).strip())except InvalidOperation as exc:raise ValueError('Price must be an unformatted whole ILS number or null') from excif not price.is_finite() or price < 0 or price != price.to_integral_value():raise ValueError('Price must be a non-negative whole ILS number or null')return int(price) if price else Nonedef seed_record(rows, id_field='id', price_field='price'):if isinstance(rows, dict):rows = [{id_field: key, price_field: value} for key, value in rows.items()]if not isinstance(rows, list):raise ValueError('Export must be a JSON ID-to-price object, JSON row array, or CSV')record = {}for index, row in enumerate(rows, 1):if not isinstance(row, dict) or id_field not in row or price_field not in row:raise ValueError(f'Row {index}: both ID and price fields are required; use explicit null for unknown price')if row.get('source') not in (None, '', 'yad2'):raise ValueError(f'Row {index}: this converter accepts Yad2 listings only')try:token, price = listing_id(row[id_field]), ils_price(row[price_field])except ValueError as exc:raise ValueError(f'Row {index}: {exc}') from excif token in record and record[token] != price:raise ValueError(f'Row {index}: conflicting prices for the same listing; reconcile the export first')record[token] = priceif not record:raise ValueError('Export contains no listings')return recorddef main(argv=None):parser = argparse.ArgumentParser(description=__doc__)parser.add_argument('export', type=Path)parser.add_argument('--monitor-name', required=True)parser.add_argument('--output', required=True, type=Path)parser.add_argument('--id-field', default='id')parser.add_argument('--price-field', default='price')args = parser.parse_args(argv)name = args.monitor_name.strip()if not name or len(name) > 60:parser.error('Monitor name must contain 1-60 characters after trimming')try:with args.export.open(encoding='utf-8-sig', newline='') as source:rows = list(csv.DictReader(source)) if args.export.suffix.lower() == '.csv' else json.load(source)record = seed_record(rows, args.id_field, args.price_field)# Exclusive create: never overwrite the export or any existing local record.with args.output.open('x', encoding='utf-8') as destination:json.dump(record, destination, ensure_ascii=False, sort_keys=True, allow_nan=False, indent=2)destination.write('\n')except (ValueError, OSError) as exc:parser.error(str(exc))print(json.dumps({'store': 'yad2-all-monitor', 'key': monitor_key(None, name),'monitorName': name, 'listings': len(record), 'recordFile': str(args.output),'uploaded': False}, ensure_ascii=False))if __name__ == '__main__':main()
Partial pagination and source-access limits
repeated_page means a requested page repeated the regular listing IDs from an earlier response. It stops that search to avoid a loop; it does not establish the cause or prove full coverage. SUMMARY.status is partial, incompleteSearches identifies affected search keys and reasons, and stopReason is incomplete_pagination when these stops prevented normal exhaustion. A repeated response is not marked as a completed page. Resuming the same checkpoint retains these stops instead of requesting those stopped searches again. This is preservation of a stop, not automatic gap recovery.
For historical runs, also inspect searchStops even if their status says succeeded. A fresh run creates fresh pagination state and normally a new dataset. Narrowing minPrice / maxPrice can be an option where source access permits it, but does not guarantee complete pagination; price ranges may exclude listings with unknown prices. There is no neighborhood input filter in this Actor. Changing filters is a fresh search, not an exact continuation of a previous snapshot.
The current collector uses rotating proxy sessions and retries blocked requests. It does not offer a no-rotation/no-block-retry mode. If your access requirements prohibit those behaviors, do not restart collection with the current configuration; offline record preparation is still possible. We cannot provide licensed Yad2 access or an official export. Obtain authorized access or a licensed export from the source before proceeding when normal access is unavailable.
For private diagnostics, contact support@leadproof.co. Share run IDs and sanitized SUMMARY/input privately, never API tokens or credentials in a public issue. Do not grant access or start additional runs solely to provide diagnostics.
Pricing
$3 per 1,000 delivered listings ($0.003 each). No start fee and no separate platform usage charge: proxy and compute are included in the listing price. Duplicates, including the same apartment on both sites, are removed before delivery and never charged. Platform usage is small: the Actor runs in 512 MB and transfers compact data.
Limits and run status
maxResults is a ceiling, not a promise; page, request and time budgets are sized from it automatically. Madlan answers are split into price ranges automatically when a search is larger than the site's page limit. Listings without a price are then only found in searches small enough to need no split.
Read SUMMARY in the run's key-value store for delivered, scanned, filtered, duplicate and cross-site duplicate counts, the site totals per search, network use and the stop reason. A run is partial when a limit stopped it, a page could not be loaded, or pagination stopped on repeated/rejected pages or a single-page-only source; delivered rows are kept. The platform can mark a gracefully finished run SUCCEEDED while the Actor's SUMMARY and status message report partial coverage.
Related LeadProof actors
- Yad2 All-in-One Scraper for Yad2 cars, motorcycles, trucks, boats and Yad2 Market items as well.
- Google Maps Scraper for Israeli real-estate agencies and other businesses.
- All actors: apify.com/leadproof.
Unofficial tool, not affiliated with Yad2, Madlan or Homeless. Use public information responsibly and comply with applicable source terms.
Built by LeadProof.