A product page needs a price that is greater than zero, a three-letter currency code and a value that matches what the shopper sees. Sale prices, strikethrough prices and member prices each have their own markup, and every currency you sell in needs its own URL. If you list products on a marketplace or look after a catalog, these rules decide whether your price markup is usable. The examples use a home espresso machine listing.
The rules come from Google Search Central: the merchant listing structured data documentation (Opens in a new tab) and the product structured data introduction (Opens in a new tab). Google may change the documentation, so check the current pages before you ship.
Which markup fits the page: snippet or merchant listing
Google describes two experiences. Product snippets suit pages where people cannot buy the product directly. Merchant listings suit pages where customers can buy from you. The price rules differ between them. The merchant listing documentation states: "Unlike product snippets, merchant listing experiences require a price greater than zero."
The deciding question is simple. Can a shopper complete a purchase on this page? If yes, build the markup to merchant listing requirements. If the page only informs and the sale happens elsewhere, snippet rules are closer to the truth.
Offer and AggregateOffer
Product snippets accept an Offer or an AggregateOffer. Merchant listings require an Offer, because the documentation says the merchant has to be the seller. Practical suggestion: one seller, one price, one Offer. A page that shows a price range across several sellers is a different kind of page and should not be forced into a single Offer with an invented price.
Only pages where a purchase is possible
The documentation says only pages where a shopper can purchase a product are eligible for merchant listing experiences. A category page that shows product cards is not that page. Put the price markup on the product page itself and make sure the price in the markup is also visible there.
Where product snippets find a price
The product structured data introduction says product snippets may use pricing data from your merchant feed if it is not present in the structured data on the page. That sentence is a reminder that the feed and the page must agree. If two systems carry the price, both need the same update.
The price must be greater than zero
For a merchant listing, the required Product properties are name, image and offers. The required Offer properties are price (or priceSpecification.price) and priceCurrency (or priceSpecification.priceCurrency). Everything else on the Offer is recommended. Table: price-related Offer properties.
| Property | Status in the documentation | Remark |
|---|---|---|
| price | Required (or priceSpecification.price) | Must be greater than zero |
| priceCurrency | Required (or priceSpecification.priceCurrency) | Three-letter ISO 4217 code |
| availability | Recommended | Covered by a separate markup topic |
| priceValidUntil | Recommended | A past date may stop the listing from showing |
| validFrom, validThrough | Recommended | The validity window of the offer |
| url | Recommended | Address of the offer page |
Quote on request and free items
An espresso machine listed as "request a quote" has no price to mark up. Writing 0 breaks the greater-than-zero rule, and inventing a number creates a gap between markup and page. The documentation has no statement on how to mark up listings that have no price. Practical suggestion: leave the Offer out and accept that such a page does not qualify as a merchant listing.
Writing the currency code and the number
The documentation asks for the currency in "three-letter ISO 4217 format". USD, GBP and EUR qualify. Symbols such as the dollar or pound sign do not replace the code.
- Write a three-letter uppercase code in priceCurrency.
- Keep the currency out of the price value.
- Use one currency per Offer.
- Confirm that the currency on the page and the code in the markup match.
Digits and separators
The merchant listing examples contain numbers such as 119.99. For shipping values it says that when a string is used, it should not include currency symbols, thousands separators or spaces. Practical suggestion: apply the same discipline to price and write digits with a decimal point only.
| Shown on the page | price value | priceCurrency |
|---|---|---|
| $1,249.00 | 1249.00 | USD |
| £899 | 899 or 899.00 | GBP |
| EUR 1.049,50 | 1049.50 | EUR |
The page can print a price however local readers expect. The markup carries the plain number. Localized display formats stay out of the data.
Rounding
If the page shows 899 and the markup says 898.99, the two values are not the same price. Practical suggestion: round where the price is calculated, then pass the rounded value to both the template and the markup.
Sale price, list price and member price
One listing can show what the shopper pays today, the previous price, and a price reserved for a membership tier. The documentation separates the three so the markup does not present the wrong amount as the active price.
Active price and priceSpecification
The active price can be set at offer level with price, or inside priceSpecification. If both are present, Google uses offers.price and ignores priceSpecification. Practical suggestion: define the active price in one place only.
Strikethrough price
For a strikethrough price, use a UnitPriceSpecification and set priceType to https://schema.org/StrikethroughPrice. The documentation says no other values are supported, and that ListPrice is allowed for a transition period. The UnitPriceSpecification needs price and priceCurrency. The documentation also warns: do not mark the current sale price with priceType.
Example scenario: a home espresso machine had a list price of 1,100 USD and is on sale for 949 USD. The active price 949 goes in price. The 1,100 goes into a separate UnitPriceSpecification marked StrikethroughPrice, and the page shows 1,100 USD crossed out. The figures are invented.
Member price
Member prices use validForMemberTier and cannot carry a priceType. Specifications that contain both priceType and validForMemberTier are ignored. Write a member price and a strikethrough price as two separate specifications.
| Price kind | Markup | Watch for |
|---|---|---|
| Active price | price and priceCurrency on the Offer | No priceType |
| Strikethrough price | UnitPriceSpecification with StrikethroughPrice | Visible crossed out on the page |
| Member price | validForMemberTier | No priceType |
Sale windows and expiry dates
The documentation says a listing may not display if priceValidUntil points to a past date. A sale that ended last week with its old date still in the markup puts the listing at risk. validFrom and validThrough describe when the offer applies.
- Write validFrom with the real start time of the sale.
- Read the end date from the system that manages the promotion.
- When the sale is extended, update the page text and the markup together.
- When it ends, remove the sale price, the strikethrough price and the date in one operation.
Practical suggestion: for a price with no end, leave the date properties out. An empty field is safer than an expired one. Keep one source of truth for the end time and let both the countdown and the markup read it.
Selling in more than one currency
The documentation is direct: when offering products for sale in multiple currencies, have a distinct URL per currency. A single address that switches currency by visitor location or language does not satisfy it. Each URL carries its own visible price and its own priceCurrency.
Setting up one URL per currency
- Pick a stable address for each currency.
- Make the visible price and the marked-up price agree on each address.
- If you connect language or country versions with hreflang, check that the address pairs are correct.
- If a currency switcher stores the visitor choice in a cookie, the currency of a page should still follow from its address.
Exchange rates
If the GBP price is calculated from the USD price, every rate change touches the markup. Practical suggestion: manage the GBP price as its own field and change it deliberately, so page and markup reflect the same decision.
A price-change workflow for a marketplace catalog
Price markup falls behind when it lives in a separate hand-edited template. A safer flow lets one price source feed the visible text, the markup and the feed.
- The price is updated in the database.
- The page template prints the visible price from it.
- The same template writes the JSON-LD price from the same field.
- The feed is built from that field, or automatic item updates are on.
- Caches are purged so an old copy cannot keep serving the old price.
Generate the markup in the first server response. Markup that a script adds later in the browser may take longer to reflect a change.
Roles on a marketplace
| Data | Source | Updated by | Checked by |
|---|---|---|---|
| Active price | Seller dashboard | Seller | Catalog manager |
| Currency code | Platform setting | Platform team | Catalog manager |
| List price | Seller dashboard | Seller | Catalog manager |
| Markup template | Template code | Platform team | SEO owner |
| Feed output | Feed generator | Platform team | SEO owner |
Practical suggestion: bind every row to a named role. Unowned rows are the ones that go stale.
Validating seller input
Sellers type "$949", "949,00" or pick the wrong currency. A template that copies this straight into markup produces an invalid price. Accept only numbers in the price field, offer currencies from a list, and convert and check the value before the template prints markup. If validation fails, print no markup for that listing.
When a price must change in an emergency
Suppose a seller enters 94.90 instead of 949 and notices an hour later. Fix the price source first. Then let the page, the markup and the feed re-read it, and purge caches. Whether the page stays on sale during the correction is a business decision, and the documentation has no statement on handling a wrong price. Practical suggestion: remember that correcting the markup alone does not change what the shopper pays at checkout, so check the cart amount as part of the same fix.
A minimum setup for a small seller
A seller with only a few listings can still do three things well: keep prices in one table, have the page template read that table, and write promotion end dates in a calendar. Get the active price and the currency right before adding strikethrough or member specifications. Each extra specification is one more thing to keep current, and an unmaintained specification does more harm than a missing one.
Handing the rules to a developer
A short brief saves rework: state that merchant listing pages need a price over zero, that priceCurrency takes an ISO 4217 code, that the sale price carries no priceType, that member prices use validForMemberTier, and that each currency has its own URL. Add the test table from the test plan as the acceptance test. A developer who has the rules and the tests can finish without a meeting.
Worked example: a home espresso machine in USD and GBP
A seller offers one espresso machine through a marketplace, with a US page priced in USD and a UK page priced in GBP. All numbers are invented for the example.
| Item | US page | UK page |
|---|---|---|
| Active price | 949 USD | 799 GBP |
| Strikethrough | 1,100 USD, crossed out on page | None, no markup either |
| Member price | None | None |
| URL | Own address | Own address |
| Source | Price table | Price table |
When the sale on the US page ends, four things change together: the price returns to 1,100, the strikethrough is removed, the end date is cleared or moved, and the feed refreshes. If any one of the four is skipped, page, markup and feed drift apart. The UK page keeps its own price and is untouched.
Test plan before release
Do not assume markup works because it was written once. Try each case on a sample listing after every template change. Table: test cases and expected output.
| Case | Expected | Failure sign |
|---|---|---|
| Regular price | Numeric price, currency USD | Text in price |
| Sale price | Sale amount in price, list price as StrikethroughPrice | Sale amount carries priceType |
| Ended sale | No strikethrough, no old date | Past date remains |
| No price | No Offer | price of 0 |
| GBP version | Own URL, currency GBP | Two currencies at one address |
| Member price | validForMemberTier without priceType | Member spec carries priceType |
Three-way comparison
Read one price from three places: the page, the markup and the cart. If they differ, first decide in the price source which one is right, then fix the wrong surface, then purge caches and read again. Fixing before deciding spreads the wrong number.
Related fields that move with price
The documentation lists availability, hasMerchantReturnPolicy, itemCondition, shippingDetails and url as recommended Offer properties. A correct price next to a stale availability value is still an inconsistency. The product structured data introduction also recommends sharing shipping costs, especially free shipping. Practical suggestion: keep the product price in price and shipping in shippingDetails, instead of folding delivery into one number.
Auditing an existing catalog for price problems
A catalog that has run for a year carries price markup written by several people and several template versions. An audit starts from a sample, not from the whole catalog. Pick listings that cover each case in the test table: regular, sale, ended sale, no price, second currency and member price. Read the page, the markup and the cart for each one.
Sampling plan
| Sample | What to read | What it proves |
|---|---|---|
| Five listings per seller group | Visible price against marked-up price | Whether seller input reaches the template intact |
| Every listing with a strikethrough price | Crossed-out amount on the page | Whether the list price is really visible |
| Every listing with an end date | priceValidUntil against today | Whether expired dates remain |
| Every second-currency page | Own address and priceCurrency | Whether currency URLs are separate |
Practical suggestion: record each finding with the listing address, the field, the value found and the value expected. A list in that shape can be handed to a developer without further explanation.
Reading the output of a template change
After fixing a template, run the same sample again. If one fix introduces a new failure, the sample shows it before the catalog does. Keep the sample stable between audits so results can be compared.
Deciding what to fix first
- Pages where the marked-up price differs from the visible price, because shoppers see one number and the data says another.
- Pages with price of 0 or missing priceCurrency, because required Offer properties are broken.
- Pages with expired priceValidUntil, because the documentation names a past date as a reason a listing may not display.
- Pages where the sale price carries priceType, because the documentation warns against it.
- Currency pages that share an address.
Language versions and currency pages
A marketplace that serves English and Turkish shoppers may also serve more than one currency. Language and currency are separate decisions. One language can be sold in two currencies, and one currency can appear on several language versions. The documentation requires a distinct URL per currency, not per language, so a French and an English page that both charge in EUR do not by themselves violate it, while an English page that switches between USD and GBP does.
Keeping the price identical across language versions
When two language versions use the same currency, they should carry the same price. Practical suggestion: both versions read the same price field, so a change appears in both at once. The markup differs only in language-dependent text such as the product name.
Hreflang and price pages
If you connect versions with hreflang, make sure each address in the pair is the page that really carries that currency and price. A mismatched pair sends a shopper from a page with one price to a page with another. Hreflang itself is a separate topic and sits outside the price rules.
Decision rule: where does each price go?
Ask whether the shopper pays this amount today. If yes, it is the price on the Offer and it has no priceType. If no, and it is a list price shown crossed out, it is a UnitPriceSpecification with StrikethroughPrice. If only a membership tier sees it, it is a separate specification with validForMemberTier. If it is not visible on the page, it does not go into the markup.
Pre-release checklist
- Can a shopper buy on this page? If so, follow merchant listing rules.
- Is price greater than zero?
- Is priceCurrency a three-letter ISO 4217 code?
- Does price contain only digits and a decimal point?
- Do offers.price and priceSpecification agree, or is only one present?
- Is the list price marked StrikethroughPrice and crossed out on the page?
- Does the active sale price avoid priceType?
- Does the member price use validForMemberTier without priceType?
- Is priceValidUntil still in the future?
- Does each currency have its own URL?
- Do page, cart and feed show the same amount?
Mistakes to avoid
Currency left blank
A price with no priceCurrency loses a required Offer property.
The sale price marked with priceType
The documentation says not to mark the current sale price with priceType. That property belongs to the strikethrough price only.
Two currencies on one address
A page that changes currency by visitor choice does not meet the one-URL-per-currency rule.
An expired promotion date
A past priceValidUntil is named in the documentation as a reason the listing may not display. Clear the date on the day the sale ends.
A strikethrough price that exists only in the markup
If the crossed-out price is not visible on the page, the markup describes something the shopper cannot see.
A range forced into an Offer
Product snippets accept AggregateOffer and merchant listings do not. Putting an invented single price on a range page does not make it a merchant listing.
Frequently Asked Questions
Can I use a price of 0 in merchant listing markup?
No. The merchant listing documentation requires a price greater than zero, unlike product snippets. If a listing has no price, leave the Offer out.
Which value should priceCurrency take for pounds?
The documentation asks for the three-letter ISO 4217 format, so GBP. A pound sign is not a substitute.
How do I mark up a crossed-out list price?
Use a UnitPriceSpecification with priceType set to https://schema.org/StrikethroughPrice, plus price and priceCurrency. The documentation says no other priceType value is supported, and that ListPrice is allowed for a transition period.
Should the current sale price carry priceType?
No. The documentation says not to mark the current sale price with the priceType property. The sale amount goes in price.
Can one URL show both USD and GBP?
The documentation asks for a distinct URL per currency when you sell in multiple currencies. Give each currency its own address with its own price and priceCurrency.
What happens if priceValidUntil is in the past?
The documentation says your listing may not display if priceValidUntil indicates a past date. Update or remove the date when a sale ends.
Conclusion
Reliable price markup answers three questions: is the price greater than zero, is the currency a three-letter ISO 4217 code, and does the marked-up value equal the one on the page? The active price lives in price. A crossed-out list price uses StrikethroughPrice and a member price uses validForMemberTier, each in its own specification. Every currency gets its own URL and every promotion gets an end date that is kept current. When page, cart and feed all read one price source, these rules need far less manual policing.
To see price markup inside the full product schema, read the product schema guide for ecommerce. For delivery and refund information next to the price, see return policy and shipping markup. If you want your listings in front of more shoppers, look at the options on the ad spaces page.
Post a comment
Please log in to leave a comment. Login