> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Extract real estate listings and property data

> Get property details from Zillow, Redfin, Idealista and other real estate sites as clean JSON with Zenrows Extract, from one listing to thousands.

Turn property pages into clean JSON: price, address, bedrooms, bathrooms, living area, agent and photos. Use `extract=standard` and Zillow, Redfin and Idealista return the same field names, so their listings land in one table without mapping. Then go from a search results page to every listing on it with Batch.

**Uses:** [Extract](/extract/setup) with [`extract=standard`](/extract/setup#extraction-modes) and [`extract_fields`](/extract/setup#selecting-the-fields-you-need), [Batch](/batch/introduction) and [Adaptive Stealth Mode](/fetch/features/adaptive-stealth-mode) (`mode=auto`)

## Ask your AI agent

With the [Zenrows MCP server](/mcp/overview) connected to Claude, Cursor or any MCP client, paste this prompt. Your agent picks the right Zenrows tools and settings on its own.

```text theme={"dark"}
Use Zenrows to get the first 10 listings from https://www.redfin.com/city/30818/TX/Austin.
Then scrape all 10 listing pages in one batch and give me a table with address, price,
bedrooms, bathrooms and living area for each. The same prompt works for hundreds of listings.
```

## Extract one property page

Send the listing URL with `extract=standard`. Add `extract_fields` to keep the groups and fields you need: `price` and `location` keep whole groups, `property.bedrooms` keeps one field.

<CodeGroup>
  ```python Python (Requests) theme={"dark"}
  import json
  import requests

  params = {
      "apikey": "YOUR_ZENROWS_API_KEY",
      "url": "https://www.redfin.com/TX/Austin/5804-Ginre-Cv-78759/home/31311780",
      "mode": "auto",
      "extract": "standard",
      "extract_fields": "property.title,property.property_type,property.bedrooms,property.bathrooms,property.living_area,property.living_area_unit,price,location",
  }

  response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
  response.raise_for_status()

  listing = response.json()["parsed"]
  print(json.dumps(listing, indent=2))
  ```

  ```javascript Node.js theme={"dark"}
  const params = new URLSearchParams({
    apikey: "YOUR_ZENROWS_API_KEY",
    url: "https://www.redfin.com/TX/Austin/5804-Ginre-Cv-78759/home/31311780",
    mode: "auto",
    extract: "standard",
    extract_fields: "property.title,property.property_type,property.bedrooms,property.bathrooms,property.living_area,property.living_area_unit,price,location",
  });

  const response = await fetch(`https://api.zenrows.com/v1/?${params}`, {
    signal: AbortSignal.timeout(180_000),
  });
  if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);

  const { parsed: listing } = await response.json();
  console.log(listing.property.title, listing.price.display, listing.property.bedrooms);
  ```

  ```bash cURL theme={"dark"}
  curl "https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fwww.redfin.com%2FTX%2FAustin%2F5804-Ginre-Cv-78759%2Fhome%2F31311780&mode=auto&extract=standard&extract_fields=property.title%2Cprice.display"
  ```
</CodeGroup>

Listings sell and expire. Swap in a current listing URL from the site you're working with; any Zillow, Redfin or Idealista property page works the same way.

## Example output

```json theme={"dark"}
{
  "location": {
    "street_address": "5804 Ginre Cv",
    "city": "Austin",
    "district": "Jollyville",
    "region": "TX",
    "postal_code": "78759",
    "country": "US",
    "latitude": 30.3963431,
    "longitude": -97.7686819
  },
  "price": {
    "value": 1039000,
    "currency": "$",
    "display": "$1,039,000",
    "price_per_area": 305,
    "period": null,
    "deposit": null,
    "qualifier": null,
    "recurring_fee": null,
    "value_max": null
  },
  "property": {
    "title": "5804 Ginre Cv, Austin, TX 78759",
    "property_type": "Single-family",
    "bedrooms": 3,
    "bathrooms": 3.5,
    "living_area": 3404,
    "living_area_unit": "sq ft"
  }
}
```

## Pick standard or native fields

`extract=standard` returns the canonical real estate schema. `extract=auto` returns the site's own extraction, which carries everything that site shows, such as Redfin's climate risks, schools, tax history and payment estimate.

| Group in `standard` | Fields |
| - | - |
| `property` | `title`, `property_type`, `listing_type`, `listing_status`, `bedrooms`, `bathrooms`, `rooms`, `living_area`, `living_area_unit`, `land_area`, `year_built`, `parking_spaces`, `features`, `description`, `listing_id`, `listing_url`, `published_date` |
| `price` | `value`, `value_max`, `currency`, `display`, `qualifier`, `price_per_area`, `period`, `deposit`, `recurring_fee` |
| `location` | `street_address`, `city`, `district`, `region`, `postal_code`, `country`, `latitude`, `longitude` |
| `agent` | `name`, `phone`, `agency_name`, `agency_url`, `agency_logo` |
| `media` | `primary_image`, `images`, `image_count`, `floor_plans`, `virtual_tour_url` |

Use `standard` when you merge several sites into one dataset. Use `auto` when you work with one site and want every detail it publishes.

## Collect listing URLs from a search page

Search and city pages list many properties. For search pages, use `extract=auto`, and keep the listing links with `extract_fields`:

```python Python (Requests) theme={"dark"}
from urllib.parse import urljoin
import requests

search_url = "https://www.redfin.com/city/30818/TX/Austin"
params = {
    "apikey": "YOUR_ZENROWS_API_KEY",
    "url": search_url,
    "mode": "auto",
    "extract": "auto",
    "extract_fields": "listings.listing_url,listings.list_price_usd",
}

response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
response.raise_for_status()

listings = response.json()["parsed"]["listings"]
listing_urls = [urljoin(search_url, item["listing_url"]) for item in listings]
print(len(listing_urls), listing_urls[:2])
```

```text Output theme={"dark"}
41 ['https://www.redfin.com/TX/Austin/5804-Ginre-Cv-78759/home/31311780', 'https://www.redfin.com/TX/Austin/2841-San-Gabriel-St-78705/home/31409609']
```

Redfin returns relative links, so `urljoin` turns them into full URLs. Field names on search pages follow each site: send one request without `extract_fields` to read them.

## Extract every listing with Batch

Send the listing URLs to Batch as one job, with `extract=standard` in `zenrows_params`. Batch runs them in parallel and retries transient failures, and each result is the same JSON a single request returns.

```python Python (SDK) theme={"dark"}
# pip install zenrows requests
import json
from urllib.parse import urljoin
import requests
from zenrows import ZenRowsBatchClient

API_KEY = "YOUR_ZENROWS_API_KEY"
search_url = "https://www.redfin.com/city/30818/TX/Austin"

# 1. Read the search page and collect listing URLs
params = {
    "apikey": API_KEY,
    "url": search_url,
    "mode": "auto",
    "extract": "auto",
    "extract_fields": "listings.listing_url",
}
response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
response.raise_for_status()
listing_urls = [urljoin(search_url, item["listing_url"]) for item in response.json()["parsed"]["listings"]]
print(f"Found {len(listing_urls)} listings")

# 2. Extract every listing in one Batch job, with the canonical schema
client = ZenRowsBatchClient(api_key=API_KEY)
job = client.submit_regular(
    listing_urls,
    zenrows_params={"mode": "auto", "extract": "standard"},
)
run = job.run.wait(timeout=900)
print(f"{run.stats.successful}/{run.stats.total} listings extracted")

# 3. Read each result
for task in run.results(status="successful"):
    listing = json.loads(run.download_task_to_memory(task))["parsed"]
    print(listing["property"]["title"], "|", listing["price"]["display"], "|",
          listing["property"]["bedrooms"], "bd")
```

```text Output (trimmed) theme={"dark"}
Found 41 listings
41/41 listings extracted
1411 W 10th St, Austin, TX 78703 | $5,999,500 | 7 bd
901 St Elmo Cir, Austin, TX 78745 | $699,000 | 3 bd
810 W Johanna St, Austin, TX 78704 | $2,495,000 | 5 bd
```

To save every listing as a row in a spreadsheet, flatten each `parsed` object with `pandas.json_normalize` and call `to_csv`. For Node.js, the REST API and the CLI, see [Scrape a list of URLs](/academy/scrape-from-a-list-of-urls).

## Customize it

| Change | How |
| - | - |
| Every detail a site publishes | `extract=auto` instead of `standard`, see [Extraction modes](/extract/setup#extraction-modes) |
| Listings from a specific country | Add `proxy_country`, for example `es` for Idealista, see [Proxy country](/fetch/features/proxy-country) |
| Rentals, other cities or filters | Change the search URL; the code stays the same |
| A site that isn't prepared yet | Ask for it with `POST /v1/extract/prepared-domains`, see [Prepare a new domain](/academy/extract/auto-extraction#prepare-a-new-domain) |
| Fresh listings every morning | Turn the Batch job into a [scheduled job](/academy/batch/scheduled-jobs) |
| A webhook when the job finishes | Add `webhook` to the job, see [Get notified when a Batch job finishes](/academy/batch/webhooks) |

## Troubleshooting

* **`400` with `REQS004` naming `extract_fields`**: a field name doesn't exist on this page. The error lists the available fields; copy names from it, or omit `extract_fields`.
* **`403` with `REQS007`**: this kind of page on the domain isn't prepared yet. [Prepare it](/academy/extract/auto-extraction#prepare-a-new-domain) with the URL in `example_urls`.
* **A listing returns `404` or `410`**: the property was sold or removed. Collect fresh URLs from the search page before each run.
* **`422` with `no_data_extracted`**: the page loaded without its data. Retry with `js_render=true&premium_proxy=true&wait=3000` instead of `mode=auto`.

## Related recipes

<CardGroup cols={2}>
  <Card title="Get structured JSON from any page" icon="wand" href="/academy/extract/auto-extraction">
    How Extract works on any prepared domain, and how to add a new one.
  </Card>

  <Card title="Scrape a list of URLs" icon="list" href="/academy/scrape-from-a-list-of-urls">
    Batch from Python, Node.js, cURL, the CLI and MCP.
  </Card>

  <Card title="Run a Batch job on a schedule" icon="calendar" href="/academy/batch/scheduled-jobs">
    Re-extract listings daily or weekly with no server to run.
  </Card>

  <Card title="Crawl a website from a start URL" icon="sitemap" href="/academy/scrape-and-crawl-from-a-seed-url">
    Discover every listing URL across a site's pages.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.