priceValidUntil on Shopify: why Google keeps showing the wrong price

Short answer. A missing priceValidUntil is a non-critical warning: Shopify's built-in structured_data filter never emits it, the product stays eligible for the rich result, and Google documents no effect on the price it shows. The field only causes trouble when it carries a date already in the past, which Google says can stop the listing from displaying. A stale price on Google usually comes from elsewhere: no recrawl since your price change, a sale price written into priceSpecification while price keeps the old figure, or malformed price data. Check those in that order.

Want to know if your store has this right now? Check my store, free 2 min · read-only

You search your own product on Google, or check a Shopping result, and the price shown doesn't match what's actually on your Shopify store: sometimes an old price from before your last change, sometimes no price rich result at all. Nothing is wrong in Shopify admin: the product is active, the price field is correct, checkout charges the right amount. The mismatch only exists on Google's side, which is the shape every silent Google visibility failure takes.

Merchants hit this and get told to wait. In a Shopify Community thread titled "Google Still Showing Old Prices for Sold-Out Products (Shopify & Merchant Center Issue)" (27 February 2025), the advice offered is "It sounds like Google is caching the old prices, which can take time to update," followed by requesting a recrawl and refreshing the feed. Waiting is often the right call, but not always: an expired end date or a sale price sitting in the wrong field will not fix itself with time. The five causes below tell the two apart.

What priceValidUntil is, and why Shopify leaves it out

priceValidUntil is a property of the Offer object inside Product structured data. Schema.org defines it in a single line, "the date after which the price is no longer available", and it expects a date. It is the one field in the offer that declares an end date for the price.

Google lists it as a recommended property, and specifically under merchant listing structured data, the markup that applies to pages where a shopper can actually buy from you. It is absent from Google's product snippet guidance, which is part of why it gets missed: if you learned Product schema from the snippet documentation, you never met this field at all.

Shopify does not add it for you. The built-in structured_data Liquid filter that Dawn and most modern themes use to generate Product JSON-LD emits an offers object with @id, @type, availability, price, priceCurrency and url. Shopify's own documentation shows the complete output, and priceValidUntil is not among those fields.

Is a missing priceValidUntil an error, or just a warning?

Worth settling before you panic-edit a theme, because the answer changes how urgently you should treat it. Google's Rich result report overview splits every reported issue into two kinds. Critical issues invalidate the item: "An invalid item has at least one critical issue preventing it from appearing as a rich result." Everything else is non-critical, and the report puts those in a separate table it calls "Improve item appearance": which, in Google's words, "Shows non-critical issues affecting structured data on your site."

A recommended property like priceValidUntil falls on the non-critical side. Google's own definition of a valid item is explicit: "A valid item is an item that doesn't have any critical issues and can appear on Google as a rich result." So a product missing this field is still eligible for the rich result, and the report is not telling you the page is broken. Which reported warnings on a Shopify product actually cost you a listing, and which are structural, is a longer question.

That is genuinely reassuring, and for a price with no end date it is the whole story: there is nothing urgent to fix. The case Google does act on is the opposite one, a date that is present but already in the past: see cause 2 below.

How to confirm it yourself in 5 minutes

  1. Run a site: search for the affected product URL and compare the price shown in the snippet against your live storefront price.
  2. Paste the same URL into Google's Rich Results Test and open the detected Product markup, check the offers object for a priceValidUntil field and whether its date is in the future.
  3. In Google Search Console, open Enhancements → Merchant listings and read the "Improve item appearance" table, not just the error list: a missing recommended field is reported there rather than as an error, which is why it is easy to scroll past. Merchant listings and product snippets are two different reports with two different requirement tables, and this field belongs to the first.
  4. If you also run Merchant Center for Shopping ads, check its Diagnostics tab for "mismatched value (price)", that's a related but distinct feed-level issue, not the same as this Search-side symptom.

StoreCanary checks this on your store

Want this checked automatically? We inspect the Offer/price schema on every product page in your catalog: read-only, no admin access, about 2 minutes.

Scan my store for free

Five causes, and how to tell which one is yours

1. priceValidUntil is missing: the warning, not usually the cause. This is the default state of a stock Shopify theme, not a merchant mistake: as above, the field simply isn't among the ones Shopify's built-in filter emits, so it has to be added deliberately via a theme edit or an app. Check it first so you can rule it out: on most stores it is true, and on its own it does not explain a stale price. Per Google's own merchant listing documentation, it is a recommended property of the Offer type, so its absence is a non-critical issue that leaves the item eligible, and Google documents no effect of that absence on the price it displays. If this is all you find, your stale price has one of the causes below.

2. priceValidUntil was set for a past promotion and never updated. A sale or discount app writes a priceValidUntil matching the promo's end date, the sale ends, and nothing goes back to refresh or remove that date. Google's documentation is explicit here: "Your listing may not display if the priceValidUntil property indicates a past date." For sale pricing Google asks you to "provide both a start and an end date/time to clearly define the sale period" (the start coming from validFrom, the end from validThrough or priceValidUntil) and to keep the start earlier than or equal to the end. A leftover expired date is worse than having none.

3. Google simply hasn't recrawled since your last price change. Indexing lag alone can produce a stale price for days to weeks, and it is the most common explanation: it is the one the thread above lands on. Requesting reprocessing in URL Inspection once the price has changed shortens the wait. If the URL itself moved rather than the price, a redirected product page is a different failure with a different signature.

4. Malformed price or currency fields. Less common, but worth a quick check: price must be a bare number string with no currency symbol, and priceCurrency must be a valid ISO 4217 code. A stray symbol or the wrong code can make Google discard the price signal from your structured data entirely and fall back to whatever it last crawled from the visible page. A price of zero is the extreme version of the same defect, and Google rejects that one outright: see the invalid value for category [price] error.

5. The discounted price is in priceSpecification, but Google is reading price. Worth a look whenever a sale is running and Google is showing the pre-sale figure. Google's rule is unambiguous: "If you use both the offers.price and offers.priceSpecification properties to encode an active price, Google will use the price provided through the offers.price property and ignore the offers.priceSpecification property." So a discount app that writes the sale price into priceSpecification while leaving the original in price produces exactly this symptom, a correct storefront, a correct checkout, and a stale number on Google. If the number Google shows disagrees with your landing page rather than lagging it, that is the Merchant Center price mismatch, which has its own outcome and its own fix.

Sale prices: where the start and end dates belong

Causes 2 and 5 both come back to the same thing, and it is the part of this markup with the most places to put a value: a sale has two prices and a period, and Google reads the period from different properties depending on where you put the active price. Google's merchant listing documentation defines the two date fields in one line each. validFrom is "The start date and time when the price is valid, in ISO 8601 format." validThrough is "The end date and time when the price is valid, in ISO 8601 format." Both are recommended properties, not required ones, so their absence lands in the same "Improve item appearance" table as a missing priceValidUntil.

The placement rule is where the trouble starts. Both date fields can sit in either of two places, and which one is correct depends on where the active price is written. If your offer's price holds the sale price, the dates belong on the Offer alongside it. If instead the sale price is defined inside a PriceSpecification node — "typically one without the priceType property when a StrikethroughPrice value is also present" — the dates belong on that node, and Google names only validFrom and validThrough for that case. Split them across the two and you have declared a price in one object and its validity period in another.

And there is one asymmetry that catches people moving between the two shapes: Google notes that "the priceValidUntil property isn't applicable to the PriceSpecification type." So priceValidUntil is an Offer-only field. Move your sale price down into a specification node and carry priceValidUntil with it, and you have moved the end date somewhere it does not apply. Google's guidance is to use either validThrough or priceValidUntil for the end — not both — and to "Provide both a start and an end date/time to clearly define the sale period", including the time and timezone for accuracy.

The second price — the crossed-out original — has its own property, and the accepted value changed. Google's current instruction is that "The current, active price automatically becomes a sale price when you provide a second price with the original, strikethrough price and mark it with a priceType property of value https://schema.org/StrikethroughPrice", and the documentation adds that "for a transition period, ListPrice is also allowed". If an older app or a theme snippet from a few years ago is writing ListPrice, it is not broken — but it is the older of the two spellings, and it is worth knowing which one your page emits before you debug anything else.

What this looks like on real storefronts, measured on 28 September 2026. I fetched one product page from each of ten live Shopify stores with a Googlebot user agent and parsed every JSON-LD block in the served HTML. On five of the ten, the product I fetched was carrying a compare_at_price — a visible, active discount. Across all ten stores, not one declared a sale period in structured data at all: validFrom 0 of 10, validThrough 0 of 10, priceValidUntil 0 of 10, priceSpecification 0 of 10, and neither StrikethroughPrice nor ListPrice anywhere. Ten stores is a sample, not a rate, and it is not a claim about Shopify's own filter. What it does show is that the shape exists and is easy to land on: a page whose markup says a price is current and says nothing whatever about a sale, while the storefront shows a crossed-out original beside it. Check your own before assuming yours is different.

Two smaller observations from the same probe, both worth a moment. Only three of the ten emitted a Product or ProductGroup in the served HTML at all; on the others there was no product markup for Google to read on arrival, which is a different problem and a different report. And of the two that did carry an offer object, neither matched the stock filter's key set: one emitted @type, availability, itemCondition, price, priceCurrency and url — an extra field, and no @id — and the other emitted a list of offers carrying sku and itemOffered. Price is not the only thing Google resolves for itself off a product page: the image in the result is selected the same way, from whatever the page declares. Both are shapes something other than the built-in filter produced. That matters for this article's central question, because an app or a custom snippet that already rewrites your offer is also the thing most likely to write a date into it.

The practical consequence is short. Read your own offer object before you add anything: find where the active price is written, then put both dates in that same object, use one end-date property rather than two, and diary the end date. Whatever you change, Search Console reports it on its own crawl schedule, so nothing you see there is live. A sale period declared correctly and then left behind after the promotion is cause 2, which is the one failure mode here that Google says can withhold the listing entirely.

The other offer fields Shopify leaves out — and why they are not the same problem

The gap that produces a missing priceValidUntil is not specific to the price. Google's merchant listing documentation lists priceValidUntil, shippingDetails and hasMerchantReturnPolicy as recommended properties of the same object, Offer. Shopify's structured_data filter emits exactly six keys inside offers, @id, @type, availability, price, priceCurrency and url. None of the recommended three is among them. On a stock theme they are absent by construction, and they turn up together in the same "Improve item appearance" table.

Google publishes the message they produce. Its rich result error messages page gives the template as Missing field "property name", with a one-line remedy: "Add the missing property name. This can also occur if the name is an empty string." Read literally, that is three fields and one fix. It is not, and the difference decides whether a theme edit is worth making.

For priceValidUntil, the remedy is the whole story: it is the case this article covers, and the fix below is all of it. For the other two, Google documents an override chain that the price field has none of. The shipping policy documentation publishes a fixed order of precedence (the Content API first, then settings in Merchant Center or Search Console, then product-level markup, then organization-level markup) and states the consequence outright: "if you provide both shipping policy markup on your site and shipping policy settings in Search Console, Google will only use the information provided in Search Console."

So a merchant pastes a shipping block into the theme, the warning stays, and nothing in the message distinguishes "you did not provide this" from "you provided it and Google used a higher-precedence source instead". The template is a bare field name either way. Those two fields, the precedence order in full, and the Shopify shipping-profile trap behind them are covered here; this section exists so you do not spend an afternoon editing a theme for a field your markup was never going to control.

The fix

  • Make sure no priceValidUntil is sitting on a past date. If you run time-limited prices, the sale app should set it to the real promo end date and clear it when the sale ends. If your prices have no end date, leaving the field out is fine; if you want the warning gone anyway, compute it live rather than hardcoding it, so it can never expire on its own:
    "priceValidUntil": "{{ 'now' | date: '%s' | plus: 2592000 | date: '%Y-%m-%d' }}"
    (the example above adds 30 days; it clears the warning and does nothing else.)
  • Audit any sale/discount app for a leftover expired date — check its settings for a hardcoded promo end date that outlived the promo itself.
  • Request reprocessing in Search Console's URL Inspection tool for the affected URL once the fix is live, rather than only waiting for the next organic crawl.
  • Validate price and priceCurrency formatting with the Rich Results Test while you're in there.
  • Check that the active price sits in offers.price, not only in offers.priceSpecification: if both carry an active price, Google reads the first and ignores the second.
  • If you run sale prices, declare validFrom as well as the end date — Google asks for both a start and an end to define the sale period, so a promotion carrying only an end date leaves the window half-declared. The start goes in validFrom, the end in validThrough or priceValidUntil, with the start earlier than or equal to the end.
  • Do not paste shipping or returns markup as a reflex. For shippingDetails and hasMerchantReturnPolicy, check first whether a Merchant Center or Search Console setting is already supplying them: per the precedence order above, that source wins over anything in your theme, so the theme edit can leave the warning exactly where it was.

None of this shows up as an error anywhere in Shopify admin: the product looks completely correct on your side. The only visible symptom is a shopper-facing price on Google that quietly drifted out of sync with your store, which is exactly the kind of gap that costs a sale before the shopper ever reaches checkout.

Is this happening on your store right now?

Merchant Center will not tell you until it disapproves a listing. The product is live in your store, and Google Shopping is showing a different price, the wrong availability, or nothing at all. The only way to know is to check the feed the way Google reads it.

  • 90+ checks, the way Google reads your product feed. Price and availability mismatches, missing identifiers, redirect chains.
  • Read-only. No app to install, no admin access, nothing changes on your store.
  • The exact fix for each page, in plain English, so you or your developer can act the same day.

Across full-catalog scans of more than 90,000 Shopify product pages, about 1 in 4 stores had at least one critical Google visibility issue.

Scan my store for free

Free scan: issue types, counts and first fixes. Full report on every product page, not a sample: $49 once, no subscription.

Next in this cluster