Structured data mastery

Offer schema: pricing, availability, inventory

Pricing, availability, sale states, pre-orders, backorders. Offer schema is small in size and large in consequence — the single block AI surfaces lean on most to decide whether a product is real, in stock, and priced correctly.

11 min read Updated May 10, 2026

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:

Offer block

Required
price, priceCurrency, availability

Recommended
priceValidUntil, gtin/mpn/sku, itemCondition, url, seller

Shipping & returns
shippingDetails, hasMerchantReturnPolicy

Inventory granularity
inventoryLevel, availabilityStarts, handlingTime

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:

Common failures: free-text strings ("In stock", "Available now") and trailing whitespace on the enum URL. Use the URL form exactly.

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:

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:

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:

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:

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.