Sold Price, Last Sold Price, and Asking Price: Which Field Should You Use?
A status-by-status guide to price, lastSoldPrice, dateSold and the last Sold event, with code that keeps asking prices, rents and sale prices in separate columns and handles undisclosed sales.
One word, three different numbers
Price sounds like one idea, but a property response carries at least three. There is the current listing's price, which may be an asking price or a monthly rent. There is the price of the last recorded sale. And there are prices attached to individual events in priceHistory, such as a listing, a price change or a pending sale. Mixing them is the fastest way to publish a wrong chart: an asking price plotted next to a sale price, a rent averaged with a list price, or a pending price reported as what a home sold for.
The rule that fixes almost every case is simple. Read homeStatus first, then decide what price means for that status. Everything else in this guide is a careful version of that rule.
The fields involved
These fields all appear in the property response and in every successful batch item. They come from the property's public Zillow page at collection time, and any of them can be null.
| Field | Meaning | Watch out for |
|---|---|---|
| price | The current listing price. A monthly rent for rentals. The sale price for a recently sold home | Null when the home is not listed or a sale price is not disclosed. Never zero |
| homeStatus | The status code that tells you which meaning price has | OTHER and OFF_MARKET usually mean not listed |
| lastSoldPrice | The source's last sale price for the home | Can be an old sale, and can arrive without a date |
| dateSold | Date of the last sale | Often null for a home that is listed again |
| priceHistory | Source events with date, event label and price | A Pending sale price is the asking price at that moment |
| calculated.last_sale_price | Price of the most recent Sold event in priceHistory | Null when that sale's price is undisclosed; an older price is never used |
| calculated.last_sale_date | Date of that Sold event | Present even when the price is not |
Decide by status
Use the table below as the mapping from status to columns. The important part is the second column: for a listed home price is an ask, for a rental it is a monthly rent, and only for a recently sold home is it a sale price. For homes that are not listed, there is no current price at all, even though the source may show a Zestimate.
| homeStatus | What price means | Sale columns to fill |
|---|---|---|
| FOR_SALE, PENDING, COMING_SOON, FORECLOSURE | Asking price of the current listing | Previous sale, from the last Sold event or lastSoldPrice |
| FOR_RENT | Monthly asking rent | Previous sale, if any, kept separate from rent |
| RECENTLY_SOLD | Sale price, or null when not disclosed | Sale date from dateSold or the last Sold event |
| OTHER, OFF_MARKET | Usually null: the home is not listed | Previous sale only; the Zestimate stays an estimate |
Undisclosed sales are missing, not cheap
Some states do not make sale prices public, and the source may show a home as sold with no price. In that case price, lastSoldPrice and the price on the Sold event can all be null. The API never turns that into zero. Your code should not either, and it should not fall back to something that looks close.
The tempting fallback is the event just before the sale. A Pending sale event often carries a price, but that number is the asking price when the home went under contract, not the amount paid. In a recorded test response for a sold home with an undisclosed price, the Pending sale event carried a price and the Sold event did not. Reporting the pending price as the sale price would have been a confident, specific and wrong number.
A second trap is an older sale. A home that sold years ago with a disclosed price and again recently without one can still have the old figure in lastSoldPrice. The calculated last_sale_price field avoids this by design: it reads only the most recent Sold event and returns null if that event's price is missing. Use it, or apply the same rule yourself.
Split prices into named columns
The function below takes the data object of one response and returns separate columns for asking price, monthly asking rent, sale price and previous sale. It never copies a value from one meaning to another, and it records notes when a sale price is undisclosed or a previous sale has no date. Use it when you export to a spreadsheet or load a database, so that every downstream user sees the meaning in the column name.
"""price_fields.py: split one property response into clearly named price columns."""
LISTED = {"FOR_SALE", "PENDING", "COMING_SOON", "FORECLOSURE"}
def last_sale(data):
"""Most recent sale: the dated Sold event first, then the lastSoldPrice field."""
calc = data.get("calculated") or {}
if calc.get("last_sale_date"):
# if this sale's price is undisclosed, keep None; never borrow an older sale's price
return calc.get("last_sale_price"), calc["last_sale_date"], "priceHistory"
if data.get("lastSoldPrice") is not None:
return data["lastSoldPrice"], data.get("dateSold"), "lastSoldPrice"
return None, None, None
def price_fields(data):
status = data.get("homeStatus")
out = dict(status=status, asking_price=None, monthly_asking_rent=None, sale_price=None,
sale_date=None, previous_sale_price=None, previous_sale_date=None, notes=[])
price, sold_price, sold_date, basis = data.get("price"), *last_sale(data)
if status == "RECENTLY_SOLD":
out["sale_price"] = price # null when the sale price is not disclosed
out["sale_date"] = data.get("dateSold") or sold_date
if price is None:
out["notes"].append("sale price not disclosed by the source")
else:
if status in LISTED:
out["asking_price"] = price
elif status == "FOR_RENT":
out["monthly_asking_rent"] = price # a monthly rent, never a sale price
else:
out["notes"].append("not listed: no current asking price")
out["previous_sale_price"], out["previous_sale_date"] = sold_price, sold_date
if sold_price is not None and sold_date is None:
out["notes"].append("previous sale price has no date in this response")
if basis:
out["notes"].append(f"last sale read from {basis}")
return out
Test the edge cases before you trust it
These tests use illustrative records shaped like real responses: a condo listed again after an earlier sale, a sold home with an undisclosed price and a priced pending event, an off-market home with an old sale, and a rental. The undisclosed case is the one that matters most. If your version ever returns the pending price as a sale price, the test fails.
Run them with plain Python. They need no network and no key.
# test_price_fields.py: illustrative records, not real homes
from price_fields import price_fields
listed = {"homeStatus": "FOR_SALE", "price": 689000, "lastSoldPrice": 615000, "dateSold": None,
"calculated": {"last_sale_price": 615000, "last_sale_date": "2024-05-17"}}
r = price_fields(listed)
assert r["asking_price"] == 689000 and r["sale_price"] is None
assert (r["previous_sale_price"], r["previous_sale_date"]) == (615000, "2024-05-17")
undisclosed = {"homeStatus": "RECENTLY_SOLD", "price": None, "lastSoldPrice": None,
"dateSold": "2026-02-24",
"priceHistory": [{"date": "2026-02-24", "event": "Sold", "price": None},
{"date": "2026-02-09", "event": "Pending sale", "price": 400000}],
"calculated": {"last_sale_price": None, "last_sale_date": "2026-02-24"}}
r = price_fields(undisclosed)
assert r["sale_price"] is None and r["sale_date"] == "2026-02-24" # the pending price is NOT the sale price
off_market = {"homeStatus": "OTHER", "price": None, "zestimate": 313400,
"lastSoldPrice": 168500, "dateSold": "2023-06-16",
"calculated": {"last_sale_price": 168500, "last_sale_date": "2023-06-16"}}
r = price_fields(off_market)
assert r["asking_price"] is None and r["previous_sale_price"] == 168500
rental = {"homeStatus": "FOR_RENT", "price": 2385, "calculated": {}}
assert price_fields(rental)["monthly_asking_rent"] == 2385
print("ok")
Use the columns without blending them
Once prices sit in named columns, most analysis mistakes become visible. A median of asking_price only includes listed homes. A sale_price chart only includes sold homes with a disclosed price, and the count of missing values belongs in the caption. A rental column never enters a sale-price average.
If you want a growth figure, change_since_last_sale_pct in the calculated object compares the last sale with the current asking price or the Zestimate, and change_since_last_sale_basis tells you which. Show the basis next to the number: a change measured against an asking price describes what a seller hopes for, and a change measured against a Zestimate describes Zillow's estimate, not a sale.
- Name columns by meaning: asking_price, monthly_asking_rent, sale_price, previous_sale_price.
- Keep null as null in exports, and count missing values separately.
- Carry homeStatus and the collection time alongside every price.
- Treat a Pending sale price as an asking price.
- Show the basis of any calculated change.
Sources and documentation
Product details checked against the linked documentation on October 22, 2026. Code and sample inputs are illustrative unless stated otherwise.
Written by PropertiesAPI, an independent property data service. We are not affiliated with Zillow. Source data may be incomplete or change over time.