Offer is the transaction sub-object inside Product3. On Shopify, the theme's auto-emitted Product block already contains an Offer with price, priceCurrency, availability, and url1. What's missing from the auto-emission and worth adding: priceValidUntil (for sale prices), itemCondition, seller (the merchant Organization), hasMerchantReturnPolicy, and shippingDetails. Google's merchant listing documentation lists priceValidUntil, itemCondition, hasMerchantReturnPolicy and shippingDetails as recommended Offer properties, and prefers store-wide return and shipping policies on Organization5. seller is a Schema.org property Google doesn't list.
Offer is one of the seven product fields Shopify's own Catalog optimisation doc names as material for AI platform consideration — "Price, availability, and key features"2. The Offer object is where price and availability live in JSON-LD, which means Offer is the schema field AI shopping engines hit first when deciding whether a Shopify product is buyable, in stock, and shippable to the user.
§01Definition
What Offer schema is
Per Schema.org v30.0, Offer is 'an offer to transfer some rights to an item or to provide a service.' The default business function is selling. On a Shopify PDP, Offer is the JSON-LD sub-object inside Product that describes the transaction itself — price, priceCurrency, availability, itemCondition, url, seller, and the sub-objects shippingDetails (OfferShippingDetails) and hasMerchantReturnPolicy (MerchantReturnPolicy).
Mental model: Product is the item; Offer is the transaction. The same physical book has one Product entity but could carry multiple Offer entities — new at $25, used at $12, library copy at $0. On Shopify, current themes such as Dawn and Horizon model a product with multiple variants (size, colour) as a ProductGroup whose hasVariant list holds one Product per variant, each with its own Offer8. A product without variants is one Product with one Offer.
§02Auto-emission
What Shopify themes emit
The Offer sub-object inside Shopify's auto-emitted Product block typically ships with @type Offer, price (the variant's price divided by 100), priceCurrency, availability (mapped to https://schema.org/InStock or OutOfStock based on inventory), and url (the product URL with the variant selected). Some themes also emit sku and itemCondition. What Shopify themes do NOT reliably emit on the Offer: priceValidUntil (the sale-end date), seller (the merchant Organization), hasMerchantReturnPolicy, shippingDetails, eligibleRegion.
For the auto-emitted Offer to be enough, the store would need to have no sales (no priceValidUntil) and to state its return and shipping policies somewhere else. That's a real option: Google recommends putting store-wide return and shipping policies on Organization, and settings in Merchant Center override markup anyway. For most Shopify stores, the auto-emitted Offer is a starting point, not an endpoint.
§03Fields
Offer fields and what they map to on Shopify
Per Schema.org v30.0, the Offer properties Shopify merchants care about are: price (Number/Text — Shopify's product.price divided by 100), priceCurrency (Text, ISO 4217 — cart.currency.iso_code, the currency the shopper sees), priceValidUntil (Date — for sale prices, when the sale ends), availability (ItemAvailability enumeration), itemCondition (OfferItemCondition — typically NewCondition), url (the PDP), seller (Organization — the merchant), sku (Text), gtin (Text — the variant's Barcode field), shippingDetails (OfferShippingDetails), hasMerchantReturnPolicy (MerchantReturnPolicy).
price — required, and above zero for merchant listings. Shopify stores prices in the currency's subunit; divide by 100.0 in Liquid. Take it from the same variant as the SKU and availability.
priceCurrency — required. ISO 4217 (USD, EUR, GBP, etc.). Use {{ cart.currency.iso_code | json }}, not shop.currency: prices render in the shopper's currency, and Shopify's multi-currency guide says structured data should carry the cart currency rather than the shop's7. How Shopify formats money for display is covered under Shopify currency format.
priceValidUntil — recommended when a sale is active. Format YYYY-MM-DD. Use the sale-end metafield or the campaign deadline.
availability — recommended by Google (price and priceCurrency are the only required Offer fields). See the next section for the enumeration map.
itemCondition — recommended. Almost always https://schema.org/NewCondition on Shopify; use UsedCondition or RefurbishedCondition only when accurate.
url — recommended. {{ shop.url }}{{ variant.url }}, which preselects the variant.
seller — optional. A Schema.org property Google's merchant listing documentation doesn't list. If you add it, reference the Organization in theme.liquid by @id.
sku — recommended. Already auto-emitted on most themes.
gtin / gtin12 / gtin13 / gtin14 — recommended. The variant's Barcode field (variant.barcode). See the GTIN leaf.
hasMerchantReturnPolicy — recommended. Schema.org MerchantReturnPolicy sub-object. Google prefers one store-wide policy on Organization and an Offer-level one only for exceptions. See the dedicated leaf.
shippingDetails — recommended. Schema.org OfferShippingDetails sub-object. Same rule: store-wide shipping goes on Organization. See the dedicated leaf.
§04Availability
The availability enumeration
Schema.org defines a fixed set of availability values: InStock, OutOfStock, PreOrder, BackOrder, SoldOut, Discontinued, LimitedAvailability. On Shopify, the mapping from inventory state to availability is straightforward but worth getting right. variant.available = true with inventory > 0 maps to InStock. variant.available = false maps to OutOfStock. PreOrder applies when variant.available = true but the product ships in the future. BackOrder applies when variant.available = true but inventory_quantity ≤ 0 with continue selling out of stock enabled.
§05priceValidUntil
priceValidUntil and Shopify sales
priceValidUntil is the date after which the current price is no longer valid. Google lists it as a recommended Offer property and warns that a listing may not display if priceValidUntil is in the past, so it only helps when it's true. On Shopify, the cleanest pattern is a product metafield (e.g. custom.sale_ends_at) that the merchant sets when configuring the sale. The Liquid block then emits priceValidUntil only when the metafield is set, avoiding stale or incorrect dates.
What NOT to do: emit a hard-coded priceValidUntil one year in the future on every product. Google's validator accepts the markup but the field loses its meaning, and on the next site audit the dates are wrong everywhere. Either tie priceValidUntil to a real campaign deadline or omit it.
§06Example
JSON-LD example — Offer with all the fields
The block below is the Offer sub-object inside your Product block on a Shopify PDP (the one that replaces the theme's structured_data line, as the Product leaf explains). It describes one variant, the selected or first available one, so price, availability, SKU and URL all agree. It includes priceValidUntil only when the sale-end metafield is present. For a product with variants, each variant Product in a ProductGroup carries its own Offer of this shape.
JSON-LDOffer sub-object for one variant, with priceValidUntil branch, availability map, and seller reference
Run the live PDP through Google's Rich Results Test. Expected: Product detected under Merchant listings, the embedded Offer parsed, zero errors. Missing recommended fields, such as hasMerchantReturnPolicy or shippingDetails when you haven't declared return and shipping policies anywhere, show as warnings, not errors. They don't block the merchant listing; they limit what Google can show about returns and shipping.
If the Rich Results Test reports the Offer has price '0.00' or a wildly wrong number, the cause is almost always Liquid: the price was not divided by 100.0, or you printed {{ variant.price }} directly instead of going through | divided_by: 100.0. A price of zero also takes the page out of merchant listings, which require a price above zero5.
§08Gotchas
Shopify gotchas on Offer
Three gotchas catch most Shopify Offer schema work. First: emitting price in cents (12500) instead of dollars (125.00) — Shopify stores prices in cents, the divided_by filter is mandatory. Second: hard-coding priceCurrency, or taking it from shop.currency. Prices render in the shopper's currency, so a store selling in several currencies ends up labelling a euro price as dollars; use cart.currency.iso_code. Third: hard-coding priceValidUntil on every product. A far-future date is meaningless, and a date that has passed can stop the listing from displaying — branch on a real metafield.
A fourth gotcha for variant pricing: product.price on a multi-variant product is the lowest price of any variant8, not the price of the variant your Offer's SKU and availability describe. Take price, SKU and availability from the same variant. For a product whose variants have different prices, use a ProductGroup with one Offer per variant. An AggregateOffer with lowPrice and highPrice is accepted for product snippets, but merchant listings need an Offer9.