Offer is the smallest Schema.org
block on a product page that has the largest consequence for AI
surface ranking. It carries price, availability, currency,
shipping, and return policy — the signals an AI surface checks
to decide whether to cite the product, what price to display in
the citation, and whether to flag the product as available.
Every Shopify theme emits some version of Offer. Most emit
two or three properties. The full block has closer to ten
properties that matter, and the gap between the default and the
complete is the gap between “this product validates” and “this
product is rankable on availability-sensitive queries.”
This guide walks through the Offer properties that matter,
the right values for each, the Shopify Liquid that produces them,
and the failure modes that come up most.
The minimal block
What every Shopify theme tends to emit:
"offers": {
"@type": "Offer",
"price": "98.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"url": "https://yourstore.com/products/wool-runner"
}
This validates. It is also missing the properties that move ranking past “exists” to “trusted.” The complete block is below; the rest of the guide explains each property and when to populate it.
The complete block
"offers": {
"@type": "Offer",
"@id": "https://yourstore.com/products/wool-runner#offer",
"sku": "ACME-WS-001",
"gtin13": "0840062303849",
"price": "98.00",
"priceCurrency": "USD",
"priceSpecification": {
"@type": "PriceSpecification",
"price": "98.00",
"priceCurrency": "USD",
"valueAddedTaxIncluded": false
},
"priceValidUntil": "2026-12-31",
"availability": "https://schema.org/InStock",
"inventoryLevel": {
"@type": "QuantitativeValue",
"value": 47
},
"itemCondition": "https://schema.org/NewCondition",
"url": "https://yourstore.com/products/wool-runner?variant=12345",
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 5.99,
"currency": "USD"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"businessDays": {
"@type": "QuantitativeValue",
"minValue": 2,
"maxValue": 5
}
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
}
},
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"applicableCountry": "US",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnMethod": "https://schema.org/ReturnByMail",
"returnFees": "https://schema.org/FreeReturn"
},
"seller": { "@id": "https://yourstore.com/#organization" }
}
This is what a fully populated Offer looks like for a US
domestic product with a defined shipping policy and a 30-day
return policy. The next sections walk through each property,
grouped by priority:
Required properties
The properties Schema.org marks required, and that any AI surface will reject the offer for missing:
price. The numeric price as a string. "98.00", not
98.00. Schema.org’s spec is more flexible than this, but in
practice some parsers prefer the string form to avoid float-
precision artifacts.
priceCurrency. ISO
4217 three-letter code:
"USD", "GBP", "EUR", "CAD", "AUD". Always
uppercase. Common Shopify failure: the currency is lower-cased
or expanded (“USD dollars”, “us dollar”) — invalid.
availability. A Schema.org enum
value URL. The full set:
https://schema.org/InStockhttps://schema.org/OutOfStockhttps://schema.org/PreOrderhttps://schema.org/BackOrderhttps://schema.org/SoldOuthttps://schema.org/Discontinuedhttps://schema.org/LimitedAvailabilityhttps://schema.org/InStoreOnlyhttps://schema.org/OnlineOnlyhttps://schema.org/PreSale
Common failures: free-text strings ("In stock", "Available now") and trailing whitespace on the enum URL. Use the URL
form exactly.
Recommended properties
The properties that aren’t required but that meaningfully lift AI surface ranking:
priceValidUntil. A future date in ISO 8601 (YYYY-MM-DD)
indicating the date through which the listed price is valid.
Used for sale prices, end-of-season pricing, and time-limited
offers. Optional for prices with no expiry, but if set in the
past, AI surfaces treat the price as stale even if the value
itself is current.
The most common Shopify pattern is to set priceValidUntil to
roughly one year in the future on every product, refreshed
periodically. The exact date doesn’t have to be precise — what
matters is that it’s in the future.
priceSpecification. A nested
PriceSpecification
that lets the offer declare whether VAT is included, whether the
price applies to a member tier, etc. Most important for EU
catalogs where VAT-inclusion needs to be explicit.
gtin13 / gtin / mpn / sku. Identifier properties at
the offer level. These can be on the Product block or the
Offer block (or both); having them on Offer lets the parser
disambiguate when a single product page has multiple Offer
blocks for different conditions or sellers.
itemCondition. A Schema.org enum
URL — typically
https://schema.org/NewCondition. Other values:
UsedCondition, RefurbishedCondition, DamagedCondition. AI
surfaces ranking commercial queries usually filter to New unless
the query is condition-specific; emitting the property explicitly
makes the offer eligible for the broadest set of queries.
url. The canonical URL the surface should link to when
citing this offer. For configurable products, this should be the
variant-specific URL (e.g., ?variant=12345) so a click lands
on the correct variant pre-selected.
seller. A reference to the
Organization selling the
product. Use the cross-reference pattern from Organization
schema
— "seller": {"@id": "https://yourstore.com/#organization"}.
Shipping and returns — the 2024+ pattern
Google’s product structured-data spec was updated in 2022 to
include shippingDetails and hasMerchantReturnPolicy as
recommended properties; emitting them now correlates with
qualifying for additional rich-result features. The two blocks
are detailed and worth getting right.
shippingDetails
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 5.99,
"currency": "USD"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 2,
"maxValue": 5
}
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
}
}
Key properties:
shippingRate— flat rate, or0for free shipping.handlingTime— business days between order placement and warehouse handoff.transitTime— business days between handoff and delivery.shippingDestination— country code or more specific region.
Catalogs with shipping rates that vary by destination should
emit shippingDetails as an array of multiple blocks, one per
destination region.
hasMerchantReturnPolicy
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"applicableCountry": "US",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnMethod": "https://schema.org/ReturnByMail",
"returnFees": "https://schema.org/FreeReturn"
}
Key values:
returnPolicyCategory—MerchantReturnFiniteReturnWindow(limited window),MerchantReturnUnlimitedWindow(always returnable), orMerchantReturnNotPermitted(final sale).merchantReturnDays— number of days from purchase or delivery (refer tomerchantReturnDayCounton Schema.org for edge cases).returnFees—FreeReturn,ReturnFeesCustomerResponsibility,ReturnShippingFees.
Catalogs with different return policies for different product categories should emit per-product values rather than a single site-wide default.
Variants — the per-offer pattern
For configurable products using
ProductGroup, each
variant carries its own Offer block. The pattern is:
- The parent
ProductGrouphas no top-leveloffersproperty. - Each
ProductinhasVarianthas its ownofferswith variant-specific price, availability, GTIN, and URL.
This lets AI surfaces filter at the variant level — “which navy, size 11 wool runner is in stock” — and return the specific SKU the buyer asked for.
If the catalog uses
AggregateOffer for
products where prices vary across variants but the per-variant
detail isn’t being exposed:
"offers": {
"@type": "AggregateOffer",
"lowPrice": "98.00",
"highPrice": "129.00",
"priceCurrency": "USD",
"offerCount": 12,
"availability": "https://schema.org/InStock"
}
AggregateOffer is acceptable but less informative than
per-variant offers. It carries a lowPrice/highPrice range
rather than a specific price per variant, and it does not carry
per-variant availability or per-variant identifiers. AI surfaces
ranking variant-specific queries have less to work with. Use
AggregateOffer only when per-variant data isn’t reliably
available; prefer the full ProductGroup + hasVariant pattern
when it is.
The Shopify Liquid
For a non-variant product, the Liquid that produces a full
Offer:
{% assign offer_url = canonical_url | default: request.origin | append: product.url %}
"offers": {
"@type": "Offer",
"@id": "{{ offer_url }}#offer",
"sku": {{ product.selected_or_first_available_variant.sku | json }},
{%- if product.selected_or_first_available_variant.barcode -%}
"gtin13": {{ product.selected_or_first_available_variant.barcode | json }},
{%- endif -%}
"price": {{ product.price | money_without_currency | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"priceValidUntil": "{{ 'now' | date: '%Y' | plus: 1 }}-12-31",
"availability": "{% if product.available %}https://schema.org/InStock{% else %}https://schema.org/OutOfStock{% endif %}",
"itemCondition": "https://schema.org/NewCondition",
"url": "{{ offer_url }}",
"seller": { "@id": "{{ shop.url }}/#organization" }
}
Notes on the Liquid:
priceValidUntilis computed dynamically as the end of next year — sufficient lead time that the date doesn’t expire before the next site refresh, but specific enough to be meaningful.gtin13is pulled from Shopify’sbarcodefield. If the catalog uses a metafield for GTIN, swap toproduct.metafields.custom.gtin.- The availability check uses Shopify’s
product.available, which returns true if at least one variant is in stock.
For variant products, the offer pattern moves inside each
hasVariant entry — see Variant
handling for the
full template.
The seven failure modes
The most common Offer errors in production:
1. Stale priceValidUntil
By far the most common offer-level issue. The date was set on
theme install (often to a fixed year like "2024-12-31") and
never refreshed. The price itself may be current, but the
expired date signals to AI surfaces that the offer is stale.
Fix: use a dynamic date (next year + 12-31, or 365 days from now) generated server-side. Verify periodically that the date is still in the future.
2. Free-text availability
availability: "In stock" instead of
availability: "https://schema.org/InStock". The string isn’t
in the Schema.org enum; the parser can’t map it. AI surfaces
treat the offer as having no defined availability and rank it
lower.
Fix: use the canonical enum URL. The Schema.org ItemAvailability page lists all valid values.
3. Lower-case priceCurrency
priceCurrency: "usd" instead of "USD". The ISO 4217
spec uses uppercase; some parsers tolerate lower-case but many
do not.
Fix: emit uppercase. Shopify’s cart.currency.iso_code returns
uppercase by default — issues are usually from hand-rolled
Liquid that lower-cases something.
4. Missing priceCurrency on multi-currency stores
For stores using Shopify Markets, the price varies by market.
Some themes emit price correctly but forget to update
priceCurrency per market — every product shows price in the
buyer’s currency but priceCurrency stays at the shop default
(“USD”). AI surfaces ranking a UK query see prices that don’t
make sense.
Fix: emit priceCurrency from the live market context, not the
shop default. See Multi-currency and international product
schema
for the full Markets pattern.
5. url pointing to a non-canonical variant URL
For variant products, the url should point to the specific
variant the offer represents (?variant=12345). Themes that emit
just the canonical product URL force the AI surface to handle
variant selection on its own.
Fix: include the ?variant= query param in the url for each
variant offer.
6. shippingDetails claiming free shipping that doesn’t exist
A theme defaults to shippingRate: 0 because the merchant filled
in “free shipping over $50” once. The structured data says
shipping is free unconditionally; the live cart applies the
threshold. AI surfaces have an inconsistent picture.
Fix: emit the actual shipping rate, or use Google Merchant
Center’s shipping settings
to express the conditional rule, and emit
shippingDetails only when there is a single canonical answer.
7. Duplicate Offer blocks
A common consequence of the JSON-LD vs. Microdata
mismatch — the
theme emits Offer in JSON-LD, an app emits another Offer
inline in Microdata, and the values disagree. AI surfaces see
two offers and rank with low confidence.
Fix: consolidate to a single offer source per page. See JSON-LD vs. Microdata vs. RDFa for the consolidation pattern.
Inventory signal granularity
The availability enum is binary at the level of a single
offer — in stock or not. AI surfaces ranking
delivery-sensitive queries (“available for next-day shipping in
California”) need more granularity. Three additional patterns
provide it:
inventoryLevel. A QuantitativeValue representing
on-hand inventory. The exact value doesn’t need to be precise —
“more than 10” is usually enough — but the presence of the
property signals that inventory data is being tracked
specifically rather than treated as binary.
shippingDetails.handlingTime. Business days between order
placement and warehouse handoff. A handling time of 0–1 business
days is what enables “ships today” surfacing on some AI
surfaces.
availabilityStarts / availabilityEnds. For pre-orders
and time-limited offers, ISO 8601 dates indicating when the
offer becomes available or ends.
The granularity is what separates a product that ranks on “any wool runners” queries from a product that ranks on “wool runners available for delivery this week in zip 94110.”
Where this matters less
Three cases where the full Offer block is overkill:
B2B catalogs with quote-based pricing. A catalog where every
price is determined by a quote process has no fixed price to
emit. The right pattern is to either skip Offer entirely on
quote-based products or emit priceSpecification with price: "0" and valueReference: "Contact for pricing" — both have
trade-offs and neither is great. The cleaner pattern is to keep
public-quote products out of AI commerce surfaces.
Digital products with unusual delivery. Software, digital
downloads, license keys — the shipping and returns properties
don’t map cleanly. Omit shippingDetails and use
hasMerchantReturnPolicy: MerchantReturnNotPermitted (or
whichever describes the policy honestly).
Subscription products. A subscription has a recurring
component the Offer block doesn’t model cleanly. Use
PriceSpecification.billingDuration
or emit the subscription as a custom property; the standard
Offer block alone doesn’t capture the subscription mechanics.
Related reading
- Product schema for Shopify —
the parent
Productblock that containsOffer. - Variant handling in product
schema — per-
variant
Offerblocks. - Out of stock, discontinued, and seasonal
products —
availability strategies beyond
InStock/OutOfStock. - Multi-currency and international product
schema —
priceCurrencyandshippingDetailsacross markets.