# Zumper Scraper - Rentals, Prices, Agents & 60+ Fields (`abotapi/zumper-scraper`) Actor

Scrape Zumper.com rental listings: apartments, houses, condos and rooms. Pulls 60+ fields per listing, including monthly rent, beds/baths, square footage, decoded amenities, agent name and phone, ratings, availability and full-resolution photos. Search by location with filters, or paste Zumper URLs.

- **URL**: https://apify.com/abotapi/zumper-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Features:** Limited permissions
- **Stats:** 3 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 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

## Zumper Scraper: Rental Listings, Rent and Availability Data

Zumper Scraper turns Zumper (zumper.com), a US rental listing site, into a clean apartment, house, condo and room dataset. Search by location with rich filters, or paste your own Zumper search, building or listing links. Every record carries monthly rent, beds/baths, square footage, decoded amenities, agent name and phone, ratings, availability and full resolution photo URLs. It also monitors listings for changes across runs (NEW, UPDATED, UNCHANGED, REAPPEARED, EXPIRED), resumes an interrupted pull, and pipes results into your apps through MCP connectors.

### Why This Scraper?

- **60+ fields per listing, core data straight from the results page.** Price, beds/baths, square footage, amenities, agent name, phone, rating and photos come back without turning on detail fetching.
- **Amenities decoded to plain text.** Amenities and building features come back as readable labels ("In-Unit Laundry", "Fitness Center", "Garage Parking"), not opaque numeric codes.
- **Full photo URLs, not just IDs.** Every image is returned as a ready to open URL.
- **Two ways to find data.** Search by location and filters, or paste specific search, building or listing URLs (multiple URLs supported).
- **Availability monitoring.** Scheduled runs classify every listing as NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED, with per campaign state you can name or share.
- **Resume long pulls.** Continue an interrupted or unbounded walk across separate runs without paying twice for what was already collected.
- **Predictable cost.** A small default result cap, per record output, and an optional per listing detail surcharge only when you turn detail fetching on.

### Use Cases

- **Rent trend and market research:** track rent ranges, price drops and new listings across neighborhoods and cities over time.
- **Apartment hunters and relocation services:** pull filtered listings (price, beds, pets) for a target city into your own site or alert tool.
- **PropTech and rental aggregators:** build a secondary Zumper sourced feed with agent contacts and photos, without visiting the site by hand.
- **Investors and property managers:** monitor competing buildings' pricing and availability in a submarket.
- **Lead generation for movers and brokers:** watch specific buildings or listings for status changes (NEW, price change, EXPIRED) and reach out through the agent contact on the record.

### Data You Get

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

| Field | Example |
| --- | --- |
| `id` | `"64606115"` |
| `url` | `https://www.zumper.com/apartment-buildings/p000000/sample-building-city-st` |
| `title` | `"1A with Balcony"` |
| `buildingName` | `"Sample Apartments"` |
| `propertyCategory` | `"apartments"` (also `"houses"`, `"condos"`, `"rooms"`) |
| `address` | `"123 Sample St"` |
| `city` / `state` / `zipcode` | `"San Francisco"` / `"CA"` / `"94103"` |
| `neighborhood` | `"SOMA"` |
| `latitude` / `longitude` | `37.7700` / `-122.4000` |
| `minPrice` / `maxPrice` | `4035` / `6360` (monthly rent, USD; the same names work as rent filters in both modes) |
| `previousPrice` | `4200` (set when the rent dropped) |
| `minBedrooms` / `maxBedrooms` | `0` / `2` (0 = studio) |
| `minBathrooms` / `maxBathrooms` | `1` / `2` |
| `minSquareFeet` / `maxSquareFeet` | `509` / `980` |
| `dateAvailable` | `"2026-01-15"` |
| `rating` / `externalRating` | `9.3` / `4.1` |
| `unitAmenities` | `["In-Unit Laundry", "Dishwasher", "Air Conditioning"]` |
| `buildingAmenities` | `["Fitness Center", "Garage Parking", "Roof Deck"]` |
| `agentName` / `brokerageName` / `phone` | `"Sample Mgmt"` / `"Sample Brokerage"` / `"(415) 555-0100"` |
| `primaryImage` / `images` / `imageCount` | `https://img.zumpercdn.com/000000000/1280x960` / `[...]` / `10` |
| `shortDescription` | `"Bright corner unit with balcony."` |
| `scrapedAt` | `"2026-01-01T00:00:00.000Z"` |
| `changeType` | `"NEW"` (only present when `incrementalMode` is on, see below) |
| `changedFields`, `firstSeenAt`, `lastSeenAt` | Incremental mode only |

With **Fetch detail pages** on, a record is additionally enriched with the full description, year built and floors, amenities grouped by category, pet policy, parking, deposits, fees, specials, income restrictions, lease terms, office hours, per unit and per floorplan availability, full agent and property manager contacts, a category by category rating breakdown, and Zumper's editorial resident feedback summary. Zumper does not publish individual resident reviews (per comment text with an author and date): the rating and feedback fields above are all of the review data that Zumper itself exposes, so `userReviews` is always present as an empty array rather than omitted.

### How to Use

1. Pick a **mode**: `search` (build a URL from the fields below) or `url` (paste your own Zumper URLs).
2. In search mode, add locations and any filters (price, beds, baths, pets); switch on **Fetch detail pages** if you need the fields above.
3. Set **Max listings** (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 apartments in a city:**

```json
{
  "mode": "search",
  "locations": ["San Francisco, CA"],
  "propertyCategory": "apartments",
  "maxListings": 20
}
```

**Search houses with filters, cheapest first, full detail:**

```json
{
  "mode": "search",
  "locations": ["Austin, TX"],
  "propertyCategory": "houses",
  "minPrice": 1500,
  "maxPrice": 6000,
  "minBedrooms": 2,
  "petsAllowed": ["dogs"],
  "sortBy": "price-asc",
  "fetchDetails": true,
  "maxListings": 50
}
```

**Paste your own Zumper URLs (search results, building or listing):**

```json
{
  "mode": "url",
  "urls": [
    "https://www.zumper.com/apartments-for-rent/new-york-ny",
    "https://www.zumper.com/apartment-buildings/p000000/sample-building-san-francisco-ca"
  ],
  "fetchDetails": true
}
```

**URL mode shapes.** Each shape below was loaded in a real run and returned listings; the `urls` field is prefilled with one example of each search shape. The field's default equals the prefill, so a URL-mode run with no links scrapes those example searches (capped by Max listings); paste your own links to scrape something else. ZIP-slug links (`/apartments-for-rent/94110-san-francisco-ca`), state-level links (`/apartments-for-rent/ca`) and some neighborhood slugs such as `/mission-district` load but carry no listings: use the city and ZIP form or a neighborhood slug that lists results.

| Shape | Example path |
|---|---|
| City search | `/apartments-for-rent/san-francisco-ca` |
| Category search (houses, condos, rooms) | `/houses-for-rent/austin-tx` |
| Paginated search (starts at that page) | `/apartments-for-rent/san-francisco-ca?page=2` |
| Filtered or sorted search | `/apartments-for-rent/san-francisco-ca?price-max=3000&bed-min=1&sort=newest` |
| Neighborhood search (use the neighborhood slug shown on Zumper) | `/apartments-for-rent/san-francisco-ca/soma` |
| City and ZIP search | `/apartments-for-rent/san-francisco-ca/94110` |
| Building or listing page | `/apartment-buildings/p13179/<slug>`, `/apartment-buildings/<id>/<slug>`, `/address/<street-city-zip-usa>` |

Facets in a pasted URL (`price-min`, `price-max`, `bed-min`, `bed-max`, `bath-min`, `pets`, `sort`) win over the run level filter fields. The tracked scope for recurring updates includes the full pasted URL. A link that is not a recognised Zumper search or building page is logged with a warning and still attempted.

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/zumper-scraper").call(run_input={"mode": "search", "locations": ["San Francisco, CA"], "maxListings": 20})
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(listing["title"], listing["minPrice"], listing["city"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/zumper-scraper').call({
  mode: 'search', locations: ['San Francisco, CA'], maxListings: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### Output views

The dataset has three views in the Console: **Listings Overview** (the main fields of every record), **Listing Details (Fetch details only)** (description, amenities, units, contacts and ratings breakdown) and **Changes (Incremental mode)** (`changeType`, `changedFields`, `detectedAt`, `priceChangeAmount`, `priceChangePercent`, `leadScore` and `stateKey` for monitoring runs). The incremental fields `detectedAt`, `leadScore`, `priceChangeAmount`, `priceChangePercent` and `stateKey` (the saved-search identity: your State key as `manual:<text>`, or the automatic `auto:<hash>` key) are added only to incremental rows and never count as a change themselves.

#### Field availability and the dedup key

Field availability can vary by region, listing type and Zumper interface changes; use `id` as the idempotency / dedup key across runs. **Fetch detail pages** defaults to off because the results page already carries the large majority of fields; turn it on when you need the full description, ratings breakdown, units or agent records.

#### Both modes, one filter set

`minPrice`, `maxPrice`, `minBedrooms`, `maxBedrooms`, `minBathrooms` and `sortBy` apply in **both** modes: in URL mode they narrow and reorder the results of a pasted search results URL after they are read, so a pasted URL can return fewer listings than the page itself shows. A pasted building or listing URL is saved as is and is never narrowed by them; each such URL returns exactly one record (its own fields, plus `units` / `floorplanListings` for unit level detail when **Fetch detail pages** is on). `locations`, `propertyCategory` and `petsAllowed` are search only: they shape the search URL this actor builds and are not re applied to results that come back.

#### How Max listings shapes a run

With one location or URL, **Max listings** simply stops the run once that many records have been pushed. With several locations, the budget is split into an equal share per location (rounded up), and each location stops at its own share, its own last page, or the site's own end of results, whichever comes first.

**Known limitation:** on each results page, the number of listings attempted is sized from the remaining budget BEFORE incremental mode classifies them, and every listing on the page is marked as handled either way. If already tracked, unchanged listings sort ahead of new or changed ones on a page, the attempts can be spent on suppressed rows and the new or changed listings further down that page are silently skipped for this run (the walk moves on to the next page, or ends, and never comes back), even though the run still finishes normally. This only bites in `incrementalMode` with a finite **Max listings** (the default is 20); a non incremental run is not affected, since nothing is suppressed. Skipped new listings show up on a later run instead. Workaround: set **Max listings** to `0` (or well above the size of the search) on recurring monitoring runs.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips everything already collected there, so you don't pay twice. A migration or **Resurrect** of a failed run also checkpoints automatically and picks back up in the same run without input.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search. Each record is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` or `EXPIRED` (only after a run that scanned the whole tracked search, and only with `emitExpired`). `stateKey` names or shares the stored state; leave it empty to derive one automatically from the location, category, filters and `fetchDetails`, so different searches never mix state with each other. `UPDATED` means any output field changed except `scrapedAt` and the fields in `ignoreFieldsForChanges`; the order of the amenity lists never counts (the photo order does, because the first photo is the cover image). Saved state is re-compared under the current ignore list, so changing it never reports every listing as `UPDATED`.
- **Ignore fields** (`ignoreFieldsForChanges`, last field of this section): field names, or dotted paths such as `units.status` that reach inside every array element, that must not count as a change. They stay in the output.
- **Price and availability monitoring** (all default off): `changeScope=priceAndStock` compares only the rent fields, `dateAvailable` and each unit's id, price and availability keys; `compactUpdates` returns `UPDATED` and `EXPIRED` rows as slim records with `priceChanges` and `variantChanges` (old and new). A compact row carries no detail page data, so it is not charged the detail enrichment event; every other row is billed as before. `expireAfterMissedScans` (default 1) holds a listing until it is missing from that many consecutive complete scans. `EXPIRED` means the listing dropped out of the results, not that it is unavailable.
- **Known limitation: `emitExpired` rows are not subject to Max listings.** Once a run qualifies to detect EXPIRED listings, every tracked listing found gone is pushed and billed, however many that is; Max listings only bounds the live NEW/UPDATED/REAPPEARED side of the same run. Budget for this on a long running campaign, or keep `emitExpired` off unless you want the full expired batch every time it fires.
- **Known limitation: searching several overlapping or adjoining locations in one run can end a location's walk early.** If two locations return some of the same listings (a shared boundary, a duplicate feed entry), a later location can reach a results page made up entirely of listings an earlier location already returned; its walk then reads that page as a repeat and stops, even though that location has more, never seen pages left. This both drops real listings from the run and, with `emitExpired` on, can mark a still live listing on the unscanned pages as EXPIRED. Workaround: avoid pairing locations whose inventories are likely to overlap in one run, or run them as separate campaigns (separate `stateKey`).
- **Known limitation: a `stateKey` shared or reused across a differently scoped run can produce false EXPIRED rows.** This includes deliberately sharing one `stateKey` between two different location, URL or search mode filter setups (in URL mode the rent, bedroom and bathroom filters are applied after reading and do not trigger this), and pasting a search URL that starts mid pagination (for example a copied `?page=3` link) under a `stateKey` that previously tracked the whole search: the listings that a differently scoped or later starting run never re-visits are not actually gone, but a complete-looking scan of the narrower scope will still mark them EXPIRED. Workaround: let `stateKey` stay empty (automatic, per scope) unless you specifically want to share state, and always paste page 1 links for a tracked campaign.
- A monitoring campaign that is bootstrapped from a `resumeFromRunId` dataset (its first incremental run only; combining the two after a baseline exists fails fast with a clear message) does not suppress the listings it bootstrapped: on the next scheduled run each of them comes back as `UPDATED`, with every field listed in `changedFields`, instead of `UNCHANGED`. Those rows are pushed and billed like any other row (plus the detail surcharge when **Fetch detail pages** is on), even though you already paid for them in the resumed run; billing does not depend on the change type, so it costs the same as if they had come back `NEW`. After that one run they are tracked normally.
- If the run is interrupted by a platform migration or resurrected while `incrementalMode` is on, listings pushed before the interruption are protected from being re-pushed or re-billed in that same run, but the automatic checkpoint saves only which listings were already handled, not their incremental classification. Their change is therefore never saved to the baseline, and on the next scheduled run they are reported (and billed) again: a listing that was `NEW` reads `NEW` again, and one that was `UPDATED` or `REAPPEARED` reads the same way again, instead of `UNCHANGED`.
- **Known limitation: a results page that loads but carries no readable listing data is treated as the end of that search.** If Zumper changes its page layout or serves an unrecognized placeholder page, the walk stops as if the search had run out, and with `emitExpired` on, every tracked listing from that page onward is marked (and billed) `EXPIRED` even though it is still live. If a monitoring run suddenly returns a large EXPIRED batch, check whether the same search still shows those listings on the site before acting on it; they come back as `REAPPEARED` on the next healthy run.

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

Optionally pipe each scraped listing into the tools you already use (Notion, Linear, Airtable, or any Model Context Protocol server) by selecting an authorized connector in the input. Each connector receives a condensed, human readable summary per item; the complete record always stays in the 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 have not 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` (build a URL from the fields below) or `url` (use your own URLs). |
| `locations` | array | New York, NY when empty | Search mode only. City, state or neighborhood, multiple supported. A bare ZIP code is not a Zumper search page (a warning is logged); use a city and ZIP link in URL mode instead. |
| `propertyCategory` | string | `apartments` | Search mode only for the category to browse. In URL mode it only labels records when the pasted URL names no category itself. |
| `petsAllowed` | array | (none) | Search mode only. Ask Zumper for listings that allow the selected pets (`dogs`, `cats`). |
| `urls` | array | sample | URL mode only. Zumper search, building or listing links. |
| `minPrice` | integer | (none) | Both modes. Minimum monthly rent (USD). |
| `maxPrice` | integer | (none) | Both modes. Maximum monthly rent (USD). |
| `minBedrooms` | integer | (none) | Both modes. Minimum bedrooms (0 = studio). |
| `maxBedrooms` | integer | (none) | Both modes. Maximum bedrooms. |
| `minBathrooms` | integer | (none) | Both modes. Minimum bathrooms. |
| `sortBy` | string | `default` | Both modes. `default`, `price-asc`, `price-desc`, `newest`, `rating-desc`. |
| `fetchDetails` | boolean | `false` | Add full description, ratings breakdown, units/floorplans, agent contacts and more. |
| `maxPages` | integer | (none, unlimited) | Result pages walked per location or URL. Leave empty to walk every page (still bounded by Max listings). |
| `maxListings` | integer | `20` | Stop after this many listings across all searches (`0` = unlimited). |
| `resumeFromRunId` | string | (none) | Continue one interrupted run, or bootstrap the first run of a new incremental campaign. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring of the SAME search. |
| `stateKey` | string | (none) | Names or shares an incremental mode state. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) unchanged listings. |
| `emitExpired` | boolean | `false` | Also return (and bill) listings tracked before but no longer found. |
| `expireAfterMissedScans` | integer | `1` | Consecutive complete scans a listing must be missing before it is `EXPIRED`. |
| `changeScope` | string | `all` | `all` or `priceAndStock` (only rent, availability date and per-unit price/availability count as a change). |
| `compactUpdates` | boolean | `false` | Slim `UPDATED` and `EXPIRED` rows with `priceChanges` and `variantChanges`. |
| `ignoreFieldsForChanges` | array | (none) | Field names or dotted paths left out of change detection (incremental mode only). |
| `proxy` | object | Residential | Proxy and connection settings; managed automatically, or supply your own proxy URLs. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output Example

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

```json
{
  "id": "64606115",
  "url": "https://www.zumper.com/apartment-buildings/p000000/sample-building-city-st",
  "padmapperUrl": null,
  "title": "1A with Balcony",
  "buildingName": "Sample Apartments",
  "propertyCategory": "apartments",
  "isBuilding": true,
  "address": "123 Sample St",
  "city": "San Francisco",
  "state": "CA",
  "zipcode": "94103",
  "neighborhood": "SOMA",
  "latitude": 37.77,
  "longitude": -122.4,
  "minPrice": 4035,
  "maxPrice": 6360,
  "previousPrice": 4200,
  "currency": "USD",
  "minBedrooms": 0,
  "maxBedrooms": 2,
  "minBathrooms": 1,
  "maxBathrooms": 2,
  "minSquareFeet": 509,
  "maxSquareFeet": 980,
  "dateAvailable": "2026-01-15",
  "listedOn": "2025-12-01T00:00:00.000Z",
  "modifiedOn": "2026-01-01T00:00:00.000Z",
  "unitAmenities": ["In-Unit Laundry", "Dishwasher", "Air Conditioning"],
  "buildingAmenities": ["Fitness Center", "Garage Parking", "Roof Deck"],
  "shortDescription": "Bright corner unit with balcony.",
  "rating": 9.3,
  "externalRating": 4.1,
  "agentName": "Sample Mgmt",
  "brokerageName": "Sample Brokerage",
  "phone": "(415) 555-0100",
  "primaryImage": "https://img.zumpercdn.com/000000000/1280x960",
  "images": ["https://img.zumpercdn.com/000000000/1280x960"],
  "imageCount": 1,
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}
```

### Plan Requirement

Zumper serves a "Client Challenge" interstitial to plain HTTP clients instead of listings. This actor loads pages in a real browser, which clears it, and in testing both an Apify residential connection and an Apify datacenter connection loaded the search pages. The Proxy & connection field defaults to Apify Residential, which is included on paid Apify plans, and the actor rotates to the next available connection when one is refused. Apify free plans do not include Apify proxies. On a free plan the log shows "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." and the run then either returns the rows the backup connection could read or fails. If the backup connection could not load the pages, the run fails with "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Zumper pages." If every connection is refused for the account, it fails with "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." Any other run in which no page loads fails with "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search." To run on a free plan, upgrade at https://apify.com/pricing or supply your own proxy URLs in the Proxy & connection field.

### FAQ

#### How much does it cost?

Billing has three events: the Apify actor start event once per run, `apify-default-dataset-item` once per listing record pushed to the dataset, and `detail-enrichment` once per listing that was enriched when **Fetch detail pages** is on (not for slim compact update rows, and not for unchanged listings that incremental mode suppresses). Nothing is charged for pages that fail or listings that are not returned. The **Pricing** tab shows the current rates. Use **Max listings** to cap the cost of a run.

#### Is it legal to scrape Zumper?

This actor collects only publicly available rental listing data. You are responsible for how you use it: follow Zumper's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing facts (rent, beds, baths, address) are generally not subject to copyright, but photos and agent supplied descriptions may carry third party 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, updated and reappeared listings (plus expired ones when `emitExpired` is on), and unchanged ones are not billed unless you turn on `emitUnchanged`.

#### Why did my run return fewer listings than I expected?

With several locations, Max listings is split into an equal share per location, and each stops at its share or its last page, whichever comes first. Filters (price, beds, baths, pets) also narrow the result set. On a recurring, incremental run over a location with a lot of steady state (unchanged) inventory, the per page budget can also be used up on suppressed rows before reaching new ones on the same page (see "How Max listings shapes a run" above). Raise Max listings, or set it to `0`, to collect more.

#### Why did my run finish with an empty dataset?

It depends on whether any page loaded. If pages loaded but nothing matched your search or filters (or incremental mode suppressed every unchanged listing), the run finishes SUCCEEDED with an empty dataset, the status message "No listings returned" and nothing billed. A results page that loads but carries no readable listing data is treated as the end of that search. If no page could be loaded at all (the site or the connection refused every request), the run ends FAILED with one of the connection messages quoted under Plan Requirement ("The proxy connection was refused for this account...", "No results: residential proxies are unavailable on this run...", or "Could not load any results page..."), so a refused connection is never reported as an empty success. Try a different location, widen the filters, or run it again in a few minutes; supplying your own proxy URLs in the Proxy & connection field can also help. If only some locations or listings could be read, you still get what was found.

#### Does emitExpired respect Max listings?

No. Max listings caps the live NEW/UPDATED/REAPPEARED side of a run; once a run qualifies to detect EXPIRED listings, every tracked listing found gone is pushed regardless of Max listings. See the known limitation under "Resume and recurring updates" above.

#### Can EXPIRED ever be wrong?

In specific cases, yes: overlapping or adjoining locations searched together in one run, or a `stateKey` that is shared or reused across a differently scoped or differently starting run (including a pasted URL that starts mid pagination), or a results page that loads without readable listing data, can make a still live listing look unscanned this run and get marked EXPIRED. See the known limitations under "Resume and recurring updates" above for the exact triggers and the workaround.

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

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

### 🔗 Want more real estate data?

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

<table>
<tr><td>🧩 <a href="https://apify.com/abotapi/padmapper-rental-listings-scraper"><b>Padmapper Scraper</b></a><br>Scrape Padmapper apartment rentals across US/CA cities: rent, beds, baths, sqft...</td><td>🏠 <a href="https://apify.com/abotapi/rent-com-scraper"><b>Rent.com Scraper</b></a><br>Extract rental listings from rent.com. Get comprehensive data including monthly rent...</td></tr>
<tr><td>🏠 <a href="https://apify.com/abotapi/trulia-scraper"><b>Trulia Scraper</b></a><br>Scrape Trulia.com property listings by location, filters, or URL. Extract prices, beds...</td><td>🏠 <a href="https://apify.com/abotapi/quintoandar-scraper"><b>QuintoAndar Scraper</b></a><br>Scrape QuintoAndar property listings across Brazil with 80+ fields, including rent, sale...</td></tr>
<tr><td>🏠 <a href="https://apify.com/abotapi/streeteasy-scraper"><b>StreetEasy Scraper</b></a><br>Scrape StreetEasy sales, rentals and in-contract listings across NYC and Jersey City...</td><td>🏠 <a href="https://apify.com/abotapi/realtor-ca-scraper"><b>Realtor.ca</b></a><br>Scrape Realtor.ca property listings with prices, descriptions, images, agent and...</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/zumper-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`):

search = build URLs from the search fields below; url = pass through your own Zumper URLs.

## `locations` (type: `array`):

Search mode only. City, state or neighborhood slug or name, e.g. "San Francisco, CA", "new-york-ny" or "Austin, TX". Leave empty to search New York, NY. A bare ZIP code is not a Zumper search page: to search one ZIP, paste its city and ZIP link (for example /apartments-for-rent/san-francisco-ca/94110) in URL mode. Multiple locations supported. Ignored in URL mode.

## `propertyCategory` (type: `string`):

Search mode only for choosing which rental category to browse. In URL mode it filters nothing: the category is read from the pasted URL, and this value is used only as the label on the saved records when the pasted URL does not name a category.

## `petsAllowed` (type: `array`):

Search mode only. Asks Zumper for listings that allow the selected pets. It is not re-checked against the returned listings, and it has no effect in URL mode.

## `urls` (type: `array`):

URL mode only. Paste Zumper search-results URLs or individual building/listing URLs. The filter and sort fields below still apply to a pasted search-results URL; a pasted building or listing URL is saved as-is. Search URLs continue from their page number onward. Supported shapes: city, category (apartments/houses/condos/rooms), paginated, filtered or sorted (price-min/max, bed-min/max, bath-min, pets, sort), neighborhood and city/ZIP search pages (the city and ZIP form, for example /apartments-for-rent/san-francisco-ca/94110), plus building or listing pages. Facets in a pasted URL win over the filter fields below.

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

Applies in BOTH search mode and URL mode. Keeps listings priced at or above this monthly rent. In URL mode the results of a pasted search-results URL are narrowed after they are read; a pasted building or listing URL is saved as-is.

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

Applies in BOTH search mode and URL mode. Keeps listings priced at or below this monthly rent. In URL mode the results of a pasted search-results URL are narrowed after they are read; a pasted building or listing URL is saved as-is.

## `minBedrooms` (type: `integer`):

Applies in BOTH search mode and URL mode. Keeps listings that can offer at least this many bedrooms; 0 = studio. In URL mode the results of a pasted search-results URL are narrowed after they are read; a pasted building or listing URL is saved as-is.

## `maxBedrooms` (type: `integer`):

Applies in BOTH search mode and URL mode. Keeps listings that start at or below this many bedrooms. In URL mode the results of a pasted search-results URL are narrowed after they are read; a pasted building or listing URL is saved as-is.

## `minBathrooms` (type: `integer`):

Applies in BOTH search mode and URL mode. Keeps listings that can offer at least this many bathrooms. In URL mode the results of a pasted search-results URL are narrowed after they are read; a pasted building or listing URL is saved as-is.

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

Applies in BOTH search mode and URL mode. Orders the listings that get saved. In URL mode the results of a pasted search-results URL are reordered after they are read; a pasted building or listing URL is saved as-is.

## `fetchDetails` (type: `boolean`):

Add full description, amenity groups, pet policy, parking, deposits, fees, lease terms, units / floorplans, full agent contacts and Zumper's category ratings + resident-feedback summary. The results page already returns ~70 fields including price, beds/baths, decoded amenities, agent name, phone, rating and photos, so leaving this off is faster and cheaper.

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

Results pages walked per location/URL. Leave empty to walk every result page (still bounded by Max listings). Pages stop early once Max listings is reached.

## `maxListings` (type: `integer`):

Stop after this many listings across all searches. 0 = unlimited.

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

Runs on any Apify plan. Connections are managed automatically to keep results flowing. You may also supply your own proxy URLs.

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

Paste a previous run ID or dataset ID to continue an unbounded walk across separate runs. Listings already collected there (matched by id) are skipped, so this run only saves the new ones. Leave empty for a normal fresh run. This continues ONE specific interrupted run; for recurring monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily/weekly or other recurring monitoring of the SAME search. The first run returns every matching listing as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED listings. UPDATED means any output field changed (rent and previous rent, availability date, beds, baths, size, amenities, description, photos, fees, specials, units) except scrapedAt and the fields you list in Ignore fields; the changed fields are named in changedFields. Order of the amenity lists never counts. Reviews are not part of this record. Turn on "Emit unchanged" or "Emit expired" only when you also want those returned (and billed). State is kept separately per location/category/filter/fetchDetails setup; use State key to name or deliberately share a monitoring campaign. Defaults off, so existing schedules are unaffected. 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 location/category/filters/fetchDetails, so 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, not when Max pages/Max listings capped it or when Resume was used. This returns (and bills) extra synthetic rows, so leave it off unless you need expiry tracking.

## `expireAfterMissedScans` (type: `integer`):

With Emit expired on, a listing is reported EXPIRED only after it was missing from this many CONSECUTIVE complete scans of the same search (default 1 = the first complete scan that misses it). A listing that comes back resets the count. EXPIRED means the listing dropped out of the search results; without a detail check a dropped listing and a delisted one look the same, so use dateAvailable and the price fields rather than reading EXPIRED as unavailable.

## `changeScope` (type: `string`):

all (default) compares every output field except the ones excluded below. priceAndStock compares ONLY the rent fields (minPrice, maxPrice, previousPrice, averagePrice, currency), dateAvailable, and for units / floorplanListings each element's id plus its price and availability keys, so description, photo, amenity or agent edits never create a billable UPDATED row. Changing scope on an explicit State key does not re-report existing listings; an automatic key is unaffected.

## `compactUpdates` (type: `boolean`):

Return UPDATED and EXPIRED rows as slim records: id, url, title, address, current price and availability, changedFields, priceChanges (old and new per field) and variantChanges (per unit ADDED, REMOVED or CHANGED). NEW rows stay full. Compact rows carry no detail-page data, so they never add the detail enrichment charge. Default off.

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

Field names (dotted paths reach inside objects and every array element, for example units.status) that must NOT count as a change. The fields stay in the output; they are only left out of the comparison and out of changedFields. scrapedAt is always ignored. Existing saved state is re-compared under this list, so adding a field never reports every listing as UPDATED.

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

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify Settings, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON; the complete record stays in the dataset. 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",
  "locations": [
    "San Francisco, CA"
  ],
  "propertyCategory": "apartments",
  "urls": [
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca",
    "https://www.zumper.com/houses-for-rent/austin-tx",
    "https://www.zumper.com/condos-for-rent/san-francisco-ca",
    "https://www.zumper.com/rooms-for-rent/san-francisco-ca",
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca?page=2",
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca?price-max=3000&bed-min=1&sort=newest",
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca/soma",
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca/94110",
    "https://www.zumper.com/apartment-buildings/p13179/potrero-1010-potrero-hill-san-francisco-ca",
    "https://www.zumper.com/apartment-buildings/1717500/blueground-san-francisco-soma-san-francisco-ca",
    "https://www.zumper.com/address/2050-divisadero-st-san-francisco-ca-94115-usa"
  ],
  "sortBy": "default",
  "fetchDetails": false,
  "maxListings": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "expireAfterMissedScans": 1,
  "changeScope": "all",
  "compactUpdates": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `detail` (type: `string`):

No description

## `changes` (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",
    "locations": [
        "San Francisco, CA"
    ],
    "urls": [
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca",
        "https://www.zumper.com/houses-for-rent/austin-tx",
        "https://www.zumper.com/condos-for-rent/san-francisco-ca",
        "https://www.zumper.com/rooms-for-rent/san-francisco-ca",
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca?page=2",
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca?price-max=3000&bed-min=1&sort=newest",
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca/soma",
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca/94110",
        "https://www.zumper.com/apartment-buildings/p13179/potrero-1010-potrero-hill-san-francisco-ca",
        "https://www.zumper.com/apartment-buildings/1717500/blueground-san-francisco-soma-san-francisco-ca",
        "https://www.zumper.com/address/2050-divisadero-st-san-francisco-ca-94115-usa"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/zumper-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",
    "locations": ["San Francisco, CA"],
    "urls": [
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca",
        "https://www.zumper.com/houses-for-rent/austin-tx",
        "https://www.zumper.com/condos-for-rent/san-francisco-ca",
        "https://www.zumper.com/rooms-for-rent/san-francisco-ca",
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca?page=2",
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca?price-max=3000&bed-min=1&sort=newest",
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca/soma",
        "https://www.zumper.com/apartments-for-rent/san-francisco-ca/94110",
        "https://www.zumper.com/apartment-buildings/p13179/potrero-1010-potrero-hill-san-francisco-ca",
        "https://www.zumper.com/apartment-buildings/1717500/blueground-san-francisco-soma-san-francisco-ca",
        "https://www.zumper.com/address/2050-divisadero-st-san-francisco-ca-94115-usa",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/zumper-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",
  "locations": [
    "San Francisco, CA"
  ],
  "urls": [
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca",
    "https://www.zumper.com/houses-for-rent/austin-tx",
    "https://www.zumper.com/condos-for-rent/san-francisco-ca",
    "https://www.zumper.com/rooms-for-rent/san-francisco-ca",
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca?page=2",
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca?price-max=3000&bed-min=1&sort=newest",
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca/soma",
    "https://www.zumper.com/apartments-for-rent/san-francisco-ca/94110",
    "https://www.zumper.com/apartment-buildings/p13179/potrero-1010-potrero-hill-san-francisco-ca",
    "https://www.zumper.com/apartment-buildings/1717500/blueground-san-francisco-soma-san-francisco-ca",
    "https://www.zumper.com/address/2050-divisadero-st-san-francisco-ca-94115-usa"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/zumper-scraper --silent --output-dataset

```

## MCP server setup

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