Recently Updated: 16 min read

Price Markup: Currency, Sale Prices, Multi-Currency

Price Markup: Currency, Sale Prices, Multi-Currency

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.

PropertyStatus in the documentationRemark
priceRequired (or priceSpecification.price)Must be greater than zero
priceCurrencyRequired (or priceSpecification.priceCurrency)Three-letter ISO 4217 code
availabilityRecommendedCovered by a separate markup topic
priceValidUntilRecommendedA past date may stop the listing from showing
validFrom, validThroughRecommendedThe validity window of the offer
urlRecommendedAddress 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 pageprice valuepriceCurrency
$1,249.001249.00USD
£899899 or 899.00GBP
EUR 1.049,501049.50EUR

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 kindMarkupWatch for
Active priceprice and priceCurrency on the OfferNo priceType
Strikethrough priceUnitPriceSpecification with StrikethroughPriceVisible crossed out on the page
Member pricevalidForMemberTierNo 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.

  1. Write validFrom with the real start time of the sale.
  2. Read the end date from the system that manages the promotion.
  3. When the sale is extended, update the page text and the markup together.
  4. 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.

  1. The price is updated in the database.
  2. The page template prints the visible price from it.
  3. The same template writes the JSON-LD price from the same field.
  4. The feed is built from that field, or automatic item updates are on.
  5. 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

DataSourceUpdated byChecked by
Active priceSeller dashboardSellerCatalog manager
Currency codePlatform settingPlatform teamCatalog manager
List priceSeller dashboardSellerCatalog manager
Markup templateTemplate codePlatform teamSEO owner
Feed outputFeed generatorPlatform teamSEO 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.

ItemUS pageUK page
Active price949 USD799 GBP
Strikethrough1,100 USD, crossed out on pageNone, no markup either
Member priceNoneNone
URLOwn addressOwn address
SourcePrice tablePrice 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.

CaseExpectedFailure sign
Regular priceNumeric price, currency USDText in price
Sale priceSale amount in price, list price as StrikethroughPriceSale amount carries priceType
Ended saleNo strikethrough, no old datePast date remains
No priceNo Offerprice of 0
GBP versionOwn URL, currency GBPTwo currencies at one address
Member pricevalidForMemberTier without priceTypeMember 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.

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

SampleWhat to readWhat it proves
Five listings per seller groupVisible price against marked-up priceWhether seller input reaches the template intact
Every listing with a strikethrough priceCrossed-out amount on the pageWhether the list price is really visible
Every listing with an end datepriceValidUntil against todayWhether expired dates remain
Every second-currency pageOwn address and priceCurrencyWhether 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

  1. Pages where the marked-up price differs from the visible price, because shoppers see one number and the data says another.
  2. Pages with price of 0 or missing priceCurrency, because required Offer properties are broken.
  3. Pages with expired priceValidUntil, because the documentation names a past date as a reason a listing may not display.
  4. Pages where the sale price carries priceType, because the documentation warns against it.
  5. 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

  1. Can a shopper buy on this page? If so, follow merchant listing rules.
  2. Is price greater than zero?
  3. Is priceCurrency a three-letter ISO 4217 code?
  4. Does price contain only digits and a decimal point?
  5. Do offers.price and priceSpecification agree, or is only one present?
  6. Is the list price marked StrikethroughPrice and crossed out on the page?
  7. Does the active sale price avoid priceType?
  8. Does the member price use validForMemberTier without priceType?
  9. Is priceValidUntil still in the future?
  10. Does each currency have its own URL?
  11. 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.

0 Comments

No comments yet for this article.

Post a comment

Please log in to leave a comment. Login