Meet Cortex - AI Powered, Expertise Refined Decision EngineYour AI Optimization Engine
SchemaAug 21, 2026·12 min read

How to Create Merchant Listing Schema: Shipping, Returns and Price Validity

TL;DR

Merchant listing is the enhanced product result, and the reason most product pages do not get it is that shipping and returns data is missing rather than that the Product markup is wrong. OfferShippingDetails and MerchantReturnPolicy are the two types that carry it, and both are structurally heavier than anything else on a product page because they encode rates, regions, handling times and return windows rather than single values. The other common failure is a conflict between the on-page markup and the Merchant Center feed, where Google sees two different prices for the same item and discounts both.

Audience

Ecommerce technical SEO leads, Shopify and headless developers, and paid media managers whose Merchant Center feed and on-page markup disagree.

Cortex

Cortex is modern marketing. Old marketing waited on people. Modern marketing fuses the efficiency of AI with the experience of experts. Meet your optimization engine.

Get Cortex

Effective

Google publishes dedicated merchant listing structured data documentation separate from its general product markup guidance, covering the shipping and returns properties required for the enhanced experience. [src]

Impact

Schema.org defines OfferShippingDetails as the type carrying shipping rate, destination and delivery time information for an offer. [src]

Action

Schema.org defines MerchantReturnPolicy as the type describing a merchant return policy, including the return window and who bears return shipping cost. [src]

Platform

Google's product structured data documentation specifies the required and recommended Offer properties, including price, priceCurrency and availability. [src]

Methodology

Cortex built this post from Google's merchant listing and product structured data documentation, the schema.org definitions for the shipping and returns types, and validation runs against live Shopify and headless product pages using the Rich Results Test.

Most product pages ship valid Product markup and never earn a merchant listing, and the reason is almost never the Product block. It is the two properties nobody adds because they are genuinely tedious to build: shipping and returns.

Google split its product documentation into two tracks for exactly this reason. There is a product snippet experience, which most correct markup earns, and there is a merchant listing experience, which is the richer treatment and asks for more. Shipping cost and return terms are the difference, because they are what a shopper comparing two results actually wants to know.

This post covers both structures in full, the two traps that silently disqualify pages that look correct, and a complete example you can adapt.

What a Merchant Listing Is

A merchant listing is the result treatment for a page where a shopper can buy the item directly, and it carries more commercial detail than a plain product snippet.

The distinction Google draws is about the page's purpose. A product page selling one item is a merchant listing candidate. A review of that product, a category page listing many items, or an editorial roundup is a product snippet candidate at best, because the shopper cannot complete a purchase there.

That has a practical consequence worth stating early. If your page is not a buyable product page, the shipping and returns work described here is not the gap holding you back, and adding it will not unlock the enhanced treatment. Check which experience you are actually eligible for before building anything.

For pages that do qualify, the added detail is what makes the result competitive. A listing showing free delivery and a 60 day return window against a competitor showing neither wins the click on identical products at identical prices.

The Offer Properties Google Expects

Everything commercial hangs off offers, and the property set divides into what is required and what earns the enhancement.

The required core is small. price and priceCurrency state what it costs, and availability states whether it can be bought, using a schema.org enumeration value rather than free text. Google's product documentation treats these as the baseline.

Four more properties do the real work.

Two identifier notes that affect matching rather than eligibility. gtin should carry the real barcode where one exists, because it is how Google reconciles your listing against other merchants selling the identical item. And sku should be your internal identifier, not a duplicate of the GTIN, since they answer different questions.

Our guide to product schema covers the Product wrapper these offers sit inside.

Shipping Details in Full

OfferShippingDetails is the most structurally involved object on a product page, because it encodes three separate things that vary independently: what shipping costs, where it goes, and how long it takes.

The rate goes in shippingRate as a MonetaryAmount. Free shipping is expressed as a value of 0 with the currency, not by omitting the property. Omission means unknown, and unknown is not the same as free.

The destination goes in shippingDestination as a DefinedRegion. Country level is the minimum, and you can narrow to regions or postal code ranges where rates differ. A single object cannot describe two different rates, so multiple rate structures need multiple OfferShippingDetails entries.

Delivery time is the part most implementations get wrong. deliveryTime is a ShippingDeliveryTime holding two distinct intervals that must not be conflated.

  • handlingTime is how long before the parcel leaves you. Business days between order and dispatch.
  • transitTime is how long the carrier takes after that.

Both are QuantitativeValue objects with a minValue, maxValue, and unitCode of DAY. A shopper reading a delivery estimate is seeing the sum, and stating them separately is what lets Google compute an accurate arrival window rather than guessing.

Two mistakes to avoid. Do not state a transit time you cannot hold during peak season, because the markup becomes a promise your fulfilment breaks in November. And if you offer several shipping speeds, model them as separate entries rather than widening one range to cover both, since a one to nine day window is useless to a shopper.

Return Policy in Full

MerchantReturnPolicy describes what happens if the shopper wants to send it back, and it needs four things to be useful.

The window goes in merchantReturnDays as an integer, paired with a returnPolicyCategory naming the policy type from the schema.org enumeration. MerchantReturnFiniteReturnWindow is the common case, with MerchantReturnNotPermitted and MerchantReturnUnlimitedWindow covering the extremes.

The method goes in returnMethod, distinguishing a mail-in return from an in-store return, which matters for retailers with physical locations.

Who pays goes in returnFees, and this is the property shoppers care about most. FreeReturn and ReturnShippingFees are different propositions and the markup should say which one applies. If the customer pays, returnShippingFeesAmount states how much.

The region goes in applicableCountry, because policies genuinely differ across borders and a single global claim is often wrong.

One structural note that saves work. If your policy is identical across your catalogue, define it once as a standalone object with an @id and reference that @id from every product rather than duplicating the block on ten thousand pages. Referencing a shared node is cleaner to maintain and means a policy change is one edit.

A Complete Working Example

This is a single product with free domestic shipping, a paid expedited option, and a 60 day free return window.

{
  "@context": "https://schema.org",
  "@type": "Product",
  "@id": "https://example.com/products/trail-runner-gtx#product",
  "name": "Trail Runner GTX Waterproof Running Shoe",
  "sku": "TR-GTX-0442",
  "gtin13": "0195842114370",
  "image": [
    "https://example.com/img/trail-runner-gtx-1x1.jpg",
    "https://example.com/img/trail-runner-gtx-4x3.jpg",
    "https://example.com/img/trail-runner-gtx-16x9.jpg"
  ],
  "description": "Waterproof trail shoe with a 4mm drop and a Vibram Megagrip outsole, built for wet rock and root.",
  "brand": { "@type": "Brand", "name": "Northfell" },
  "offers": {
    "@type": "Offer",
    "url": "https://example.com/products/trail-runner-gtx",
    "price": "164.00",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "itemCondition": "https://schema.org/NewCondition",
    "priceValidUntil": "2026-12-31",
    "shippingDetails": [
      {
        "@type": "OfferShippingDetails",
        "shippingRate": {
          "@type": "MonetaryAmount",
          "value": "0",
          "currency": "USD"
        },
        "shippingDestination": {
          "@type": "DefinedRegion",
          "addressCountry": "US"
        },
        "deliveryTime": {
          "@type": "ShippingDeliveryTime",
          "handlingTime": {
            "@type": "QuantitativeValue",
            "minValue": 1,
            "maxValue": 2,
            "unitCode": "DAY"
          },
          "transitTime": {
            "@type": "QuantitativeValue",
            "minValue": 3,
            "maxValue": 5,
            "unitCode": "DAY"
          }
        }
      },
      {
        "@type": "OfferShippingDetails",
        "shippingRate": {
          "@type": "MonetaryAmount",
          "value": "19.50",
          "currency": "USD"
        },
        "shippingDestination": {
          "@type": "DefinedRegion",
          "addressCountry": "US"
        },
        "deliveryTime": {
          "@type": "ShippingDeliveryTime",
          "handlingTime": {
            "@type": "QuantitativeValue",
            "minValue": 0,
            "maxValue": 1,
            "unitCode": "DAY"
          },
          "transitTime": {
            "@type": "QuantitativeValue",
            "minValue": 1,
            "maxValue": 2,
            "unitCode": "DAY"
          }
        }
      }
    ],
    "hasMerchantReturnPolicy": {
      "@type": "MerchantReturnPolicy",
      "applicableCountry": "US",
      "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
      "merchantReturnDays": 60,
      "returnMethod": "https://schema.org/ReturnByMail",
      "returnFees": "https://schema.org/FreeReturn"
    }
  }
}

Note what the two shipping entries accomplish. Standard delivery is free and arrives in four to seven days all in. Expedited costs 19.50 and arrives in one to three. A single entry averaging those would have described neither.

The Feed Conflict Trap

If you run Google Shopping, Google now has two descriptions of the same product and it compares them.

The failure is mundane and extremely common. Your Merchant Center feed says 164.00 because it syncs nightly. Your page says 149.00 because a promotion went live this morning. Google sees a mismatch and treats both sources as less reliable, which can suppress the enhancement you built the markup to earn.

Four places the conflict originates.

  • Promotions applied at the template level but not pushed to the feed. The most frequent cause by far.
  • Currency and locale variants where the feed carries one market and the page renders another.
  • Availability lag, where the page says in stock and the feed still says out of stock from the last sync, or the reverse.
  • Variant mismatch, where the feed itemises variants and the page markup describes only the default.

The fix is architectural rather than clever. Generate both the feed and the on-page JSON-LD from the same source of truth, at the same moment, so a price change updates both or neither. Any pipeline where the two are produced by different systems on different schedules will drift, and drift is what Google penalises.

Our guide to Merchant Center for agencies covers the feed side of that pipeline, and our post on optimising Shopify product data covers the platform-specific version.

The priceValidUntil Trap

priceValidUntil is the quietest failure in product markup because nothing appears broken.

The property states when the offer expires. Once that date passes, the offer is expired by its own declaration, and the enhanced treatment can drop even though the page is live, the price is current, and the item is in stock. Validators report no error, because the markup is structurally valid. It simply says the offer ended.

Sites that hardcode a date during a build and never revisit it hit this every year. A value of 2025-12-31 set during a 2025 launch is silently withdrawing every offer on the site through the whole of 2026.

Three ways to handle it.

  • Omit the property entirely if the price has no genuine end date. This is the right answer for most catalogues and it is safer than a wrong date.
  • Generate it dynamically as a rolling window, such as today plus 90 days, so it cannot go stale.
  • Set it to the real end date when the price genuinely expires, which is the case for a time-boxed sale.

Then add a monitoring check that alerts on any priceValidUntil in the past. It is a one-line assertion in a crawl and it catches an entire class of silent failure.

Validation and Common Errors

Validate through the Rich Results Test to confirm which experience you qualify for, and the Schema.org validator to catch type errors Google tolerates silently. The Rich Results Test explicitly distinguishes merchant listing eligibility from product snippet eligibility, which tells you whether the shipping and returns work landed.

Seven errors account for most failures.

  • Free shipping expressed by omitting shippingRate. Absent means unknown. State a value of 0.
  • handlingTime and transitTime collapsed into one figure. They are separate intervals and Google sums them.
  • availability as free text. It must be a schema.org enumeration URL, not the string In Stock.
  • A stale priceValidUntil. Silently expires the offer with no validation error.
  • Return policy duplicated per product with inconsistent values. Define it once with an @id and reference it.
  • Markup on a non-buyable page. Reviews and category pages are not merchant listings regardless of how complete the markup is.
  • Feed and page disagreeing on price or availability. Undermines both sources.

Our guide to ecommerce SEO covers where this markup sits in the wider technical picture.

Frequently Asked Questions

Do I need shipping and returns markup for a product rich result?

Not for the basic product snippet, which most valid Product markup earns. You need it for the merchant listing experience, which is the enhanced treatment showing delivery and return detail. That is the gap on most product pages.

How do I mark up free shipping?

Include shippingRate as a MonetaryAmount with a value of 0 and the correct currency. Leaving the property out signals unknown cost rather than free, so the omission loses you the benefit you were trying to show.

Can one product have several shipping options?

Yes. Provide an array of OfferShippingDetails objects, one per rate and speed combination. Do not widen a single delivery window to span standard and expedited, because the resulting estimate is accurate for neither.

What happens if my feed price and page price disagree?

Google sees two conflicting claims about the same item and treats both as less trustworthy, which can suppress the enhanced treatment. Generate the feed and the on-page markup from one source at one time so they cannot drift.

Should I always set priceValidUntil?

No. Omit it unless the price genuinely expires. A hardcoded date that has passed expires your offer silently, with no validation error to warn you, and it is one of the most common invisible failures in product markup.

Key Takeaways

  • -Merchant listing eligibility usually fails on missing shipping and returns data, not on broken Product markup.
  • -OfferShippingDetails encodes rate, destination and the two-part delivery time as separate structured values.
  • -MerchantReturnPolicy needs the window, the category, and who pays return shipping.
  • -On-page markup that disagrees with your Merchant Center feed undermines both sources.
  • -priceValidUntil on a stale date silently withdraws the offer, which is a common and invisible failure.

Ready to optimize for the AI era?

Get a free AEO audit and discover how your brand shows up in AI-powered search.

Get Your Free Audit