Use the ".priceChangeHistory" to analyze a listing's valuation behavior over it's lifecycle
Price Change History
Why track a listing's price over its lifecycle
The current listingPrice on a listing is a single snapshot. It doesn't tell you: has this seller already cut the price twice? Is the current price higher or lower than when it first listed? Is this a "price war" market with rapid successive cuts, or a stable one? priceChangeHistory answers those questions by tracking every recorded price movement on a listing, not just its current value.
This matters for:
- Negotiation leverage — a listing with 3 price drops in 60 days signals seller motivation.
- Market timing models — average days between price changes is a proxy for how "hot" or "cold" a submarket is.
- Deal scoring — comparing the current price to the original list price surfaces listings that have already meaningfully repriced toward market value.
- Lead scoring for investors/agents — repeated price cuts often correlate with sellers open to below-ask offers.
Structure
"priceChangeHistory": {
"listingPriceLow": 410000,
"listingPriceHigh": 450000,
"numberOfPriceChanges": 2,
"averageDaysBetweenPriceChanges": 21,
"listingPriceChangeList": [
{
"priceChangeDate": "2026-05-14",
"previousListPrice": 425000,
"previousPricePerSqft": 212.50,
"newListPrice": 410000,
"newPricePerSqft": 205.00,
"currentListingPriceChangeDelta": -15000,
"currentDeltaFromOriginalListPrice": -40000
}
]
}listingPriceChangeList is ordered most-recent → least-recent, so index [0] is the latest change and the last element is the original listing event.
| Field | Type | Meaning |
|---|---|---|
listingPriceLow / listingPriceHigh | int | The lowest / highest list price this listing has ever carried |
numberOfPriceChanges | int | Total count of recorded price changes |
averageDaysBetweenPriceChanges | int | Average gap between changes — a low number signals an aggressively repricing seller |
listingPriceChangeList[].priceChangeDate | date | When the change happened |
listingPriceChangeList[].previousListPrice / newListPrice | int | Price before/after this specific change |
listingPriceChangeList[].previousPricePerSqft / newPricePerSqft | float | $/sqft before/after |
listingPriceChangeList[].currentListingPriceChangeDelta | int | This change's size (negative = price cut) |
listingPriceChangeList[].currentDeltaFromOriginalListPrice | int | Cumulative movement vs. the original list price |
Available filters
| Filter | Type | Maps to |
|---|---|---|
has_price_changes | bool | Listing has ≥1 recorded price change |
listing_price_low_min/_max, listing_price_high_min/_max | num | Historical price floor/ceiling |
number_of_price_changes_min/_max | int | How many times it's repriced |
average_days_between_price_changes_min/_max | int | Repricing velocity (alias: avg_days_between_price_changes_*) |
delta_from_last_list_price_min/_max | num | Size of the most recent change (matches any change entry) |
delta_from_original_list_price_min/_max | num | Cumulative change vs. the original list price |
previous_list_price_min/_max, new_list_price_min/_max | num | Bound any individual change's before/after price |
original_listing_price_min/_max | num | The very first list price (pipeline-pending — see note below) |
current_price_per_sqft_min/_max | num | Most-recent $/sqft (pipeline-pending — see note below) |
Note:
original_listing_priceandcurrent_price_per_sqftrequire reading a specific array position (the oldest / newest entry), which isn't something Elasticsearch can filter efficiently on today. These fields are wired up and will activate automatically once the data pipeline denormalizes them onto the top-level object — no API change needed on your end.
Example queries
Find listings that have cut price at least twice and are still active:
{ "active": true, "number_of_price_changes_min": 2 }Find listings that dropped price by at least $10,000 in their most recent change:
{ "active": true, "delta_from_last_list_price_max": -10000 }Find aggressively-repricing markets (changes happening fast):
{ "city": "Austin", "state": "TX", "has_price_changes": true, "average_days_between_price_changes_max": 14 }