Sale Duration Markup: The Fields Most Stores Skip
Two additions to Google’s merchant listing documentation on 7 July 2026 closed a gap that has been producing stale sale prices in search results for years. One is a new Sale duration section covering validFrom, validThrough and priceValidUntil. The other documents Product.category for both Text and CategoryCode types. The merchant listing page has been revised since and carries a last-updated date of 8 September 2026.
Where the sale dates go
The rule is stated by node, not by property, and that is where most implementations go wrong. Google’s sale duration guidance for merchant listings sets out two placements:
- On the
Offernode. AddvalidFromand eithervalidThroughorpriceValidUntildirectly to the offer. This is correct when thepriceproperty on the offer holds the current active sale price. - On a
PriceSpecificationnode. If the sale price is defined inside a price specification, typically one without apriceTypewhere a strikethrough price is also present, putvalidFromandvalidThroughon that specific node. Google states plainly thatpriceValidUntilis not applicable to thePriceSpecificationtype.
Values are ISO 8601, and Google recommends including the time and the timezone offset for accuracy in its systems — 2025-11-30T23:59:59+00:00 rather than a bare date. The documented best practice is to supply both a start and an end, with the start earlier than or equal to the end. A sale that opens at midnight in one market and closes at midnight in another is exactly the case a bare date cannot express.
The two end-date properties are not interchangeable
Google now documents both as valid ways to say when a sale price stops, which is a change from how most stores learned to use them. The distinction is in what each one is defined to mean. The schema.org definition of priceValidUntil is “the date after which the price is no longer available”, and it is listed as used on Offer. That is why it cannot sit on a price specification, and why a store that has attached it to one has markup that validates structurally but says nothing Google will read as a sale end.
The other failure is older and more common: priceValidUntil set to a date in the past, usually because a template stamped it once at publication and nothing ever refreshed it. A price whose stated validity has expired is a price Google has been told not to trust. If your product markup is generated by a template rather than by the pricing system, that field is the one to check first.
The category property takes text, a code, or both
The category property accepts an array that mixes plain text strings and CategoryCode objects, which lets a product carry your taxonomy and Google’s at the same time. Plain text represents your own custom product category, the equivalent of the product_type feed attribute, and Google recommends keeping it under the 750-character limit. For a Google Product Category you use the CategoryCode type: set @type to CategoryCode, set inCodeSet to a Google Product Taxonomy URL, and set codeValue to either the numeric category ID, such as 2271, or the full path using > as the separator between levels.
Markup and feed have to agree
Both July additions were made to align structured data with Merchant Center feed specifications: sale duration with the sale_price_effective_date attribute, and category with product_type and google_product_category. That alignment is only useful if the two sources actually match. When a feed says a sale runs to Sunday and the markup says Friday, you have not given Google a tie-break, you have given it a reason to distrust both. The same applies to category: a feed classifying a product one way and markup classifying it another is a disagreement your store is publishing about itself. The wider point in what schema markup does and doesn’t do holds here — the markup describes the page, it does not overrule it.
A validation pass you can actually run
Take a sample of live product URLs across the templates you maintain: a plain product, one on sale, one with variants, one with a membership price. For each, confirm the offer node’s price is the price a shopper pays today, that the duration properties sit on the node holding that price, and that no priceValidUntil has drifted into a price specification or into the past. Then pull the same SKUs from the feed and compare dates and categories field by field. Run it after every promotion, not before one, because the failure mode is a sale that ended on Monday and is still marked up on Thursday. The store-side detail that usually explains the drift is covered in the product-page details that get missed, and the feed side in what Google checks in Merchant Center.