---
title: "Real Estate Listing Data: Fields That Matter for Valuation"
description: "Which fields in a Zillow search response feed a valuation model (price, livingArea, yearBuilt), which are noisy, and what listing data can't tell you."
url: https://perceptlayer.com/blog/real-estate-listing-data-valuation-fields
---

[Blog](https://perceptlayer.com/blog) / [Web data by industry](https://perceptlayer.com/blog/category/web-data)

# Real Estate Listing Data: Fields That Matter for Valuation

12 Jul 2026 Perceptlayer 10 min read

A valuation model needs a small core from each listing: price, living area, bedrooms, bathrooms, year built, lot size, property type, status and a location. The Zillow API's search endpoint returns all of those as named JSON fields for 1 credit per call. What the sample doesn't show matters just as much. This post maps the fields to model inputs and flags what to distrust.

## Key takeaways

-   The recorded `/v1/zillow/search` response carries `price.value`, `price.pricePerSquareFoot`, `livingArea`, `bedrooms`, `bathrooms`, `yearBuilt`, `lotSizeWithUnit`, `propertyType`, `listing.listingStatus`, `listing.marketingStatus` and `location` for each listing.
-   A list price is an asking price, not a sale price. Treat it as a noisy label for what homes sell for. The search endpoint accepts `home_statuses[]=recentlySold`, but we haven't recorded a sold response, so check what it returns before you rely on it.
-   The recorded response includes `taxAssessment.taxAssessedValue` (949000, `taxAssessmentYear` "2025") on the second listing, but no annual property tax, HOA fees, sale history, condition or renovation data.
-   Drop free-text and marketing fields such as `title` and `mapDotTag`, and keep `estimates.zestimate` out of your features: it is Zillow's own valuation, so a model that learns from it is circular.
-   Each search call costs 1 credit, but one query returns at most 1,000 rows ( `page * page_size` must not exceed 1000), so the recorded area's 2,058 matches need at least 103 calls at `page_size` 20.

## Which listing fields does a valuation model actually need?

A hedonic valuation model, which [breaks a home into its characteristics and estimates the value each contributes](https://en.wikipedia.org/wiki/Hedonic_regression), needs size, rooms, age, lot, type and location, plus a price target. All of them appear in a single search listing. The rest of the response (photo links, virtual tour flags, rental flags) describes how a listing is marketed, not what the home is worth.

Here is how the recorded fields map to model inputs:

| Model input | Field in the search response | Notes |
| --- | --- | --- |
| Target price | `price.value` | List price, in `currency`: `usd` |
| Price per area | `price.pricePerSquareFoot` | Supplied in the response; recompute it from `price.value` and `livingArea` to check it |
| Size | `livingArea` | Unit isn't documented; 7870 with a `pricePerSquareFoot` of 1779, and 2100 with 500, are both consistent with square feet |
| Bedrooms | `bedrooms` | Integer |
| Bathrooms | `bathrooms` | Integer in the sample |
| Age | `yearBuilt` | Convert to age at the call date |
| Lot | `lotSizeWithUnit.lotSize` and `lotSizeUnit` | The unit changes between listings: `acres` on the first, `squareFeet` on the second. Convert in code |
| Property type | `propertyType` | `singleFamily` in the sample |
| Location | `location.latitude`, `location.longitude`, `address.zipcode` | Coordinates for distance features, zip for grouping |
| Listing status | `listing.listingStatus` | `forSale` on both sample listings |
| Marketing status | `listing.marketingStatus` | `active` on the first listing, `comingSoon` on the second. A coming-soon price is a different label from an active asking price, so use this as a status feature or filter on it |
| Assessed value | `taxAssessment.taxAssessedValue` and `taxAssessmentYear` | 949000 and `"2025"` on the second listing only. An assessed value, not the annual tax bill; keep the year with it |
| Zillow's estimate | `estimates.zestimate` | 13068300 on the first listing only. Don't train on it: see below |

The [Zillow API](https://perceptlayer.com/apis/zillow) has ten endpoints. The search endpoint is the one that returns this table in bulk; the [Property endpoint](https://perceptlayer.com/apis/zillow/property) takes `zpid[]` for individual homes.

## What does the recorded response look like?

The recorded response from `/v1/zillow/search` nests everything under `data.listings`. Each listing has a `zpid` identifier, a `location` object, an `address` object and flat fields for size and type. Here is the request we recorded, centred on Seattle:

```bash
curl "https://api.perceptlayer.com/v1/zillow/search?latitude=47.6062&longitude=-122.3321&radius_mi=5&listing_category=category1&page_size=20" \
  -H "X-API-Key: $API_KEY"
```

An excerpt of the first listing it returned:

```json
{
  "data": {
    "listings": [
      {
        "zpid": 2068074790,
        "location": { "latitude": 47.592564, "longitude": -122.386925 },
        "address": {
          "streetAddress": "1124 Sunset Avenue SW",
          "zipcode": "98116",
          "city": "Seattle",
          "state": "WA"
        },
        …
        "bathrooms": 6,
        "bedrooms": 4,
        "livingArea": 7870,
        "yearBuilt": 2015,
        "lotSizeWithUnit": { "lotSize": 0.2652, "lotSizeUnit": "acres" },
        "propertyType": "singleFamily",
        "listing": {
          "listingStatus": "forSale",
          "marketingStatus": "active",
          …
        },
        "price": {
          "value": 14000000,
          "pricePerSquareFoot": 1779
        },
        "estimates": {
          "zestimate": 13068300
        },
        …
```

And an excerpt of the second listing, which reports its lot in a different unit and carries a tax assessment instead of an estimate:

```json
      {
        "zpid": 49046647,
        …
        "bathrooms": 2,
        "bedrooms": 4,
        "livingArea": 2100,
        "yearBuilt": 1974,
        "lotSizeWithUnit": { "lotSize": 3933.468, "lotSizeUnit": "squareFeet" },
        "propertyType": "singleFamily",
        "listing": {
          "listingStatus": "forSale",
          "marketingStatus": "comingSoon",
          …
        },
        "price": {
          "value": 1050000,
          "pricePerSquareFoot": 500
        },
        "taxAssessment": {
          "taxAssessedValue": 949000,
          "taxAssessmentYear": "2025"
        },
        …
```

The paging fields sit at the end of the `data` object:

```json
    "count": 20,
    "total_matching_count": 2058,
    "page": 1,
    "page_size": 20,
    …
    "paging_note": "page * page_size must not exceed 1000; up to 1000 rows come back in one request."
```

Keys are camelCase and the units are implicit, so write a small normalisation layer before anything reaches your model. For authentication and requests, see the docs on [authentication](https://docs.perceptlayer.com/authentication) and [requests](https://docs.perceptlayer.com/requests).

## Which fields are noisy or missing in this sample?

Several fields need cleaning, and several valuation inputs aren't in the sample at all. The two recorded listings already disagree with each other: one reports its lot in acres and the other in square feet, one carries a Zillow estimate and the other a tax assessment, and one is `active` while the other is `comingSoon`. Two listings are an anecdote, not a benchmark, so check your own pulls.

### Noisy fields

-   **`price.value`** is what the seller asks. It is the right target for "what is on the market" and the wrong one for "what did homes sell for". For a `comingSoon` listing it is an even earlier signal, so keep `listing.marketingStatus` next to it.
-   **`price.pricePerSquareFoot`** is supplied, but how it's computed isn't documented. In the sample, 14,000,000 divided by 7,870 is about 1,779 and 1,050,000 divided by 2,100 is exactly 500, both matching the supplied values, so recompute it from `price.value` and `livingArea` if you want a single source of truth.
-   **`title`** reads "West Seattle L&I Co 1st Of" and "Scenic Heights Supl Plat" in the sample. Neither is an address or a description. Drop it.
-   **`bathrooms`** is a count; whether it includes half baths isn't documented, so don't assume.
-   **`lotSizeWithUnit`** comes with a unit, and the unit varies. The first listing has 0.2652 `acres`; the second has 3933.468 `squareFeet`. A model that reads both as the same unit without checking `lotSizeUnit` is wrong by a factor of 43,560.
-   **`estimates.zestimate`** is Zillow's own estimate of value (13,068,300 against a list price of 14,000,000 on the first listing). It is useful as a benchmark to compare your model's output against. As a feature it makes the model circular: you would be training a valuation model partly on another valuation model's output, and the error you measure would look smaller than it is.

### Fields the response doesn't show

The recorded response does include `taxAssessment.taxAssessedValue` (949000) and `taxAssessmentYear` ("2025") on the second listing. That is an assessed value, not annual property tax, and the first listing doesn't have the field at all. Neither listing in the recorded response shows annual property tax, HOA fees, sale history, renovation status, basement or garage details, or school quality. We haven't recorded those, so check a full response in the playground on the [Property](https://perceptlayer.com/apis/zillow/property) and [Schools](https://perceptlayer.com/apis/zillow/schools) endpoint pages before you build on them.

## How do you pull a clean training table?

Request listings by area, flatten the nested objects, normalise the lot size and drop anything you can't use. The code below calls the search endpoint with the same parameters as the recorded example, keeps the fields from the mapping table and prints the first two rows. It uses `.get` throughout, because a field missing on one listing shouldn't stop the batch. The Zestimate is stored for benchmarking only; leave it out of your feature list.

```python
import os
from datetime import date

import requests

SQFT_PER_UNIT = {"acres": 43560, "squareFeet": 1}

resp = requests.get(
    "https://api.perceptlayer.com/v1/zillow/search",
    headers={"X-API-Key": os.environ["API_KEY"]},
    params={
        "latitude": 47.6062,
        "longitude": -122.3321,
        "radius_mi": 5,
        "listing_category": "category1",
        "page_size": 20,
    },
    timeout=30,
)
resp.raise_for_status()
data = resp.json()["data"]

rows = []
for item in data["listings"]:
    price = (item.get("price") or {}).get("value")
    living_area = item.get("livingArea")
    if not price or not living_area:
        continue  # no target or no size: unusable for training

    lot = item.get("lotSizeWithUnit") or {}
    loc = item.get("location") or {}
    listing = item.get("listing") or {}
    tax = item.get("taxAssessment") or {}
    unit_factor = SQFT_PER_UNIT.get(lot.get("lotSizeUnit"))
    lot_size = lot.get("lotSize")
    year_built = item.get("yearBuilt")

    rows.append({
        "zpid": item.get("zpid"),
        "price": price,
        "living_area": living_area,
        "price_per_area": round(price / living_area),
        "beds": item.get("bedrooms"),
        "baths": item.get("bathrooms"),
        "age": date.today().year - year_built if year_built else None,
        # unknown unit -> None, never a silent guess
        "lot_sqft": round(lot_size * unit_factor) if lot_size and unit_factor else None,
        "type": item.get("propertyType"),
        "lat": loc.get("latitude"),
        "lon": loc.get("longitude"),
        "zipcode": (item.get("address") or {}).get("zipcode"),
        "status": listing.get("listingStatus"),
        "marketing_status": listing.get("marketingStatus"),
        "tax_assessed_value": tax.get("taxAssessedValue"),
        "tax_year": tax.get("taxAssessmentYear"),
        "zestimate": (item.get("estimates") or {}).get("zestimate"),  # benchmark only
    })

print(len(rows), "usable of", len(data["listings"]), "listings;",
      data["total_matching_count"], "match the query")
for row in rows[:2]:
    print(row)
```

Run against the recorded request, the first two rows are the two listings above, with `lot_sqft` about 11,552 for the first and 3,933 for the second, both in square feet. To narrow the pull, the search endpoint accepts `min_price`, `max_price`, `min_beds`, `max_beds`, `living_area_min`, `lot_size_min` and `year_built_min`, and `home_statuses[]` to select for example `recentlySold`. Parameters you can page with are `page` and `page_size`, and `page * page_size` must not exceed 1000. Errors come back with an HTTP status and a code; see the [error reference](https://docs.perceptlayer.com/errors).

## How do you turn one listing into model features?

Take the second recorded listing, a typical house rather than the $14,000,000 outlier, and compute the derived features the table calls for, using a call date in 2026:

-   **Age:** 2026 − `yearBuilt` 1974 = 52 years.
-   **Lot in square feet:** `lotSize` 3933.468 with `lotSizeUnit` `squareFeet` needs no conversion, so it stays about 3,933 square feet (roughly 0.09 acres). The first listing's 0.2652 acres would need × 43,560 = about 11,552 square feet.
-   **Own price per area:** `price.value` 1,050,000 ÷ `livingArea` 2,100 = 500.
-   **Price against assessment:** `price.value` 1,050,000 ÷ `taxAssessedValue` 949,000 = about 1.11, an assessment from 2025 compared with a 2026 asking price.
-   **Status:** `marketingStatus` `comingSoon`, so a flag separates it from `active` listings.

The own price per area matches the supplied `pricePerSquareFoot` of 500. Computing it yourself means every row uses one formula, even when a field is missing or differs on other listings. For this row, the model sees a home 52 years old, on about 3,933 square feet, listed at 500 per unit of living area, before it hits the market as an active listing.

## What does a pull cost?

Every call to the search endpoint costs 1 credit, and a failed call costs nothing. So the cost of a dataset is the number of calls, not the number of listings. The recorded request shows how fast that adds up: `total_matching_count` is 2,058, and `paging_note` says `page * page_size` must not exceed 1000, so one query returns at most 1,000 rows. Here are two worked examples:

-   One area like the recorded one: 2,058 matches ÷ `page_size` 20 = 103 pages, so at least 103 calls × 1 credit = 103 credits. You can't page past 1,000 rows in a single query, so split the area into slices that each match 1,000 or fewer (a smaller `radius_mi`, a bounding box, or bands of `min_price` and `max_price`). Uneven slices cost more calls than the minimum.
-   A neighbourhood-level refresh of 2,000 search calls × 1 credit = 2,000 credits. The free tier gives 1,000 credits a month, so 1,000 of those credits are free and the other 1,000 come from a pack.

The smallest pack is Starter, with 10,000 credits. You buy the whole pack, so the second 1,000 calls draw on it and the remaining 9,000 credits stay for later pulls. A monthly refresh of 10,000 calls × 1 credit = 10,000 credits fits one Starter pack.

Credits are prepaid and purchased credits last 12 months. See [pricing](https://perceptlayer.com/pricing) for what credits cost and the [credits docs](https://docs.perceptlayer.com/credits). See [rate limits](https://docs.perceptlayer.com/rate-limits) before you pull many areas in parallel.

## When is listing data not enough for valuation?

Listing data describes asking prices, so it is weakest for homes that aren't on the market and for homes whose condition drives the price. Test any model built on it against sold prices in your own market before you trust it.

What that means for your own model:

-   **Asking prices aren't sale prices.** A model trained on asking prices learns what sellers hoped for. Add `recentlySold` listings through `home_statuses[]` and a status feature ( `listing.marketingStatus` is one), and inspect what a sold record contains first.
-   **Zillow's estimate isn't independent.** `estimates.zestimate` is another valuation. Use it to sanity-check your output, not as an input.
-   **Condition is a gap.** Two identical-looking homes can differ by a full renovation. The [Photos endpoint](https://perceptlayer.com/apis/zillow/photos) takes `zpid[]`, so you can pull images for a computer-vision step. The trade-off is a separate modelling project on top of the tabular one.
-   **Outliers distort a small pull.** The sample's first listing is a $14,000,000, 7,870-square-foot home with six bathrooms, a poor comparable for a typical house. Narrow the pull with `min_price`, `max_price` and `min_beds`, at the cost of fewer rows per call.
-   **Lending is a different bar.** [12 CFR 323.3](https://www.law.cornell.edu/cfr/text/12/323.3) requires an appraisal by a state certified or licensed appraiser for real estate-related financial transactions at institutions the FDIC regulates, with listed exemptions, so a model output doesn't replace one.

If you need area-level context rather than home-level inputs, the [Market Data endpoint](https://perceptlayer.com/apis/zillow/market-data) takes a required `family` and `geography` (one of City, County, Metro, National, Neighborhood, State, Zip). The Zillow API also has a `/v1/zillow/zestimate-accuracy` endpoint, which takes optional `state` and `metro` parameters. For a full walk-through of the search call, see [Zillow Listings API: Get Property Data as JSON](https://perceptlayer.com/blog/zillow-listings-api-json). More real estate endpoints are listed in [real estate APIs](https://perceptlayer.com/verticals/real-estate).

Next step: [get a free API key](https://perceptlayer.com/signup), open the [Zillow API](https://perceptlayer.com/apis/zillow) page and run the search call in the playground against your own market before you design features.

## APIs in this post

-   [Zillow **1 credit per call** · 10 endpoints](https://perceptlayer.com/apis/zillow)

## Frequently asked questions

### Which Zillow field should be the target for a valuation model?

Use `price.value` as the target only if you accept that it's an asking price. For a model of sale values, pull listings with `home_statuses[]=recentlySold` and inspect what the response returns for them. Don't mix active and sold prices in one training set without a status feature, and don't use `estimates.zestimate` as an input.

### Can I get listing data for a whole city in one call?

No. The recorded request reports a `total_matching_count` of 2,058, but `paging_note` says `page * page_size` must not exceed 1000, so one query returns at most 1,000 rows. Split the area with a smaller `radius_mi`, a bounding box or price bands, then page each slice. At `page_size` 20, 2,058 matches need at least 103 calls, which is 103 credits.

### How much does a valuation dataset cost to collect?

Each successful call costs 1 credit, and failed calls cost nothing. 2,000 calls use 2,000 credits. The free tier gives 1,000 credits a month after you verify an email address, with no credit card, so the other 1,000 come from a pack. The smallest, Starter, has 10,000 credits; see [pricing](https://perceptlayer.com/pricing) for what credits cost.

### Is the data complete and current?

The data is collected from publicly available sources and returned as found at the time of the call. We don't promise completeness or freshness. Fields can be absent on some listings, as `taxAssessment` and `estimates` are in the sample, so write code that tolerates missing values and record the date of each pull alongside the data.

### What happens if I run out of credits mid-pull?

The service is prepaid, with no automatic top-up. When the balance runs out, calls return `insufficient_credits` (402) and nothing more is debited. Check the `X-Credits-Remaining` header on each response, and stop the loop on a 402 so you can resume after adding credits.

real estate valuation zillow listings property data

## More from the blog

1.  [API guides](https://perceptlayer.com/blog/category/api-guides) 1 Jul 2026 Updated 26 Oct 2026 Perceptlayer 9 min read
    
    ## [Zillow Listings API: Get Property Data as JSON](https://perceptlayer.com/blog/zillow-listings-api-json)
    
    One GET to /v1/zillow/search returns listings near a point as JSON with price, beds, baths and address. Parameters, a real response, 1 credit per call.
    
2.  [API guides](https://perceptlayer.com/blog/category/api-guides) 8 Oct 2026 Perceptlayer 6 min read
    
    ## [Realestate.com.au API: Australian Listings as JSON](https://perceptlayer.com/blog/realestate-com-au-api-listings)
    
    Search realestate.com.au by location and channel (buy, rent, sold), then open each listing by URL. Parameters, a recorded Sydney response and the cost.
    
3.  [Web data by industry](https://perceptlayer.com/blog/category/web-data) 9 Sept 2026 Perceptlayer 7 min read
    
    ## [Rental Listing Data for Market Analysis: Fields That Matter](https://perceptlayer.com/blog/rental-listing-data-market-analysis)
    
    Which rental listing fields drive yield and rent-index work: rent range, beds, fees, deposit and availability date, with real HotPads and SpareRoom output.
