Tracking Price Changes on a Listing

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.

FieldTypeMeaning
listingPriceLow / listingPriceHighintThe lowest / highest list price this listing has ever carried
numberOfPriceChangesintTotal count of recorded price changes
averageDaysBetweenPriceChangesintAverage gap between changes — a low number signals an aggressively repricing seller
listingPriceChangeList[].priceChangeDatedateWhen the change happened
listingPriceChangeList[].previousListPrice / newListPriceintPrice before/after this specific change
listingPriceChangeList[].previousPricePerSqft / newPricePerSqftfloat$/sqft before/after
listingPriceChangeList[].currentListingPriceChangeDeltaintThis change's size (negative = price cut)
listingPriceChangeList[].currentDeltaFromOriginalListPriceintCumulative movement vs. the original list price

Available filters

FilterTypeMaps to
has_price_changesboolListing has ≥1 recorded price change
listing_price_low_min/_max, listing_price_high_min/_maxnumHistorical price floor/ceiling
number_of_price_changes_min/_maxintHow many times it's repriced
average_days_between_price_changes_min/_maxintRepricing velocity (alias: avg_days_between_price_changes_*)
delta_from_last_list_price_min/_maxnumSize of the most recent change (matches any change entry)
delta_from_original_list_price_min/_maxnumCumulative change vs. the original list price
previous_list_price_min/_max, new_list_price_min/_maxnumBound any individual change's before/after price
original_listing_price_min/_maxnumThe very first list price (pipeline-pending — see note below)
current_price_per_sqft_min/_maxnumMost-recent $/sqft (pipeline-pending — see note below)

Note: original_listing_price and current_price_per_sqft require 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 }