Copy-paste reference

Product schema markup examples for the four fields almost no store states

Your platform already writes the easy half of your product markup. It does not write the half that decides which store an assistant names when a shopper asks "which one should I buy". Here is the code for that half, one field at a time, with the share of 61 measured stores that state each one.

15%
state a rating
10%
state a returns policy
8%
state shipping & delivery

Measured on a real product page at 82 stores from the Tranco long tail, August 2026; percentages are of the 61 that stated machine-readable product data. Full method and numbers →

Before you paste anything

Two rules that matter more than the code below.

Every example below goes inside a <script type="application/ld+json"> block on your product template — not the homepage. Placeholders are highlighted; replace all of them.

1. Rating and review count

aggregateRating · stated by 9 of 61 stores (15%)

This is the single biggest gap we measured, and the one an assistant misses most. Asked to compare two products, a model with no rating on either side has nothing to rank by — so it ranks the third store that has one. aggregateRating sits on the Product, not on the offer.

aggregateRating (goes on the Product)
"aggregateRating": {
  "@type": "AggregateRating",
  "ratingValue": "YOUR_REAL_AVERAGE",
  "reviewCount": "YOUR_REAL_NUMBER_OF_REVIEWS",
  "bestRating": "5",
  "worstRating": "1"
}
ratingValue is the average, e.g. "4.6". reviewCount is how many reviews it is averaged from. If your reviews app renders stars in HTML but emits no markup, this is invisible to a crawler — the stars a human sees are not data.

Do not invent this one. If you have no reviews yet, leave the field out and collect some. Ratings are the field most often faked and the one most easily checked against the reviews on your own page.

2. Returns policy

hasMerchantReturnPolicy · stated by 6 of 61 stores (10%)

A returns policy is a tie-breaker in a shopping answer: between two similar products, free 30-day returns is the reason to name one of them. It lives on the Offer, not on the product, and it is machine-readable only as an enumeration — a sentence in your footer does not count.

hasMerchantReturnPolicy (goes inside offers)
"hasMerchantReturnPolicy": {
  "@type": "MerchantReturnPolicy",
  "applicableCountry": "YOUR_COUNTRY_CODE",
  "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
  "merchantReturnDays": YOUR_RETURN_WINDOW_IN_DAYS,
  "returnMethod": "https://schema.org/ReturnByMail",
  "returnFees": "https://schema.org/FreeReturn"
}
applicableCountry is a two-letter code, e.g. "US" or "DE". If the buyer pays return shipping, use "https://schema.org/ReturnShippingFees" and add a returnShippingFeesAmount. If you take no returns at all, the honest value is returnPolicyCategory "https://schema.org/MerchantReturnNotPermitted" and no merchantReturnDays.

3. Shipping cost and delivery time

shippingDetails · stated by 5 of 61 stores (8%)

The rarest field of the six, and the one shoppers ask about most directly ("what has it here by Friday"). It also lives on the Offer. Handling time is how long before you dispatch; transit time is how long the carrier takes.

shippingDetails (goes inside offers)
"shippingDetails": {
  "@type": "OfferShippingDetails",
  "shippingRate": {
    "@type": "MonetaryAmount",
    "value": "YOUR_SHIPPING_COST",
    "currency": "YOUR_CURRENCY_CODE"
  },
  "shippingDestination": {
    "@type": "DefinedRegion",
    "addressCountry": "YOUR_COUNTRY_CODE"
  },
  "deliveryTime": {
    "@type": "ShippingDeliveryTime",
    "handlingTime": {
      "@type": "QuantitativeValue",
      "minValue": YOUR_HANDLING_DAYS_MIN,
      "maxValue": YOUR_HANDLING_DAYS_MAX,
      "unitCode": "DAY"
    },
    "transitTime": {
      "@type": "QuantitativeValue",
      "minValue": YOUR_TRANSIT_DAYS_MIN,
      "maxValue": YOUR_TRANSIT_DAYS_MAX,
      "unitCode": "DAY"
    }
  }
}
Free shipping is value "0". Ship to several countries? Repeat the whole block as an array, one entry per destination — do not list several countries in one DefinedRegion with one rate unless the rate really is the same.

4. A product identifier

gtin / mpn · stated by 24 of 61 stores (39%)

The least glamorous field and the one that does the quiet work: it is how an assistant knows your listing and a competitor's listing are the same physical product. Without it you are not compared, you are just another page. Identifiers sit on the Product.

gtin and mpn (go on the Product)
"gtin13": "YOUR_13_DIGIT_BARCODE",
"mpn": "YOUR_MANUFACTURER_PART_NUMBER",
"sku": "YOUR_OWN_INTERNAL_CODE",
"brand": {
  "@type": "Brand",
  "name": "YOUR_BRAND_NAME"
}
Use the property that matches the barcode you actually have: gtin13 for EAN-13, gtin12 for UPC, gtin8, gtin14. Your sku is your own code and is not a substitute — nobody else knows it. If you make the product yourself and it has no barcode, brand plus mpn is the honest pair; delete the gtin line.

All four in one block

If your product page has no Product markup at all, this is the whole thing — the two fields your platform normally supplies plus the four it does not.

Complete Product block
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "YOUR_PRODUCT_NAME",
  "image": "YOUR_PRODUCT_IMAGE_URL",
  "description": "YOUR_PRODUCT_DESCRIPTION",
  "sku": "YOUR_OWN_INTERNAL_CODE",
  "gtin13": "YOUR_13_DIGIT_BARCODE",
  "mpn": "YOUR_MANUFACTURER_PART_NUMBER",
  "brand": { "@type": "Brand", "name": "YOUR_BRAND_NAME" },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "YOUR_REAL_AVERAGE",
    "reviewCount": "YOUR_REAL_NUMBER_OF_REVIEWS"
  },
  "offers": {
    "@type": "Offer",
    "url": "YOUR_PRODUCT_PAGE_URL",
    "price": "YOUR_PRICE_AS_A_NUMBER",
    "priceCurrency": "YOUR_CURRENCY_CODE",
    "availability": "https://schema.org/InStock",
    "hasMerchantReturnPolicy": {
      "@type": "MerchantReturnPolicy",
      "applicableCountry": "YOUR_COUNTRY_CODE",
      "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
      "merchantReturnDays": YOUR_RETURN_WINDOW_IN_DAYS,
      "returnMethod": "https://schema.org/ReturnByMail",
      "returnFees": "https://schema.org/FreeReturn"
    },
    "shippingDetails": {
      "@type": "OfferShippingDetails",
      "shippingRate": {
        "@type": "MonetaryAmount",
        "value": "YOUR_SHIPPING_COST",
        "currency": "YOUR_CURRENCY_CODE"
      },
      "shippingDestination": {
        "@type": "DefinedRegion",
        "addressCountry": "YOUR_COUNTRY_CODE"
      },
      "deliveryTime": {
        "@type": "ShippingDeliveryTime",
        "handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" },
        "transitTime": { "@type": "QuantitativeValue", "minValue": 1, "maxValue": 5, "unitCode": "DAY" }
      }
    }
  }
}
</script>
availability is InStock, OutOfStock, PreOrder or BackOrder — state the one that is true right now, and make sure it changes when your stock does. Stale availability is the one error an assistant can catch you on the same day.

Where it goes

Reference: every property here is defined by schema.org/Product and schema.org/Offer. We use them because that is the vocabulary product pages already speak — the same markup that has fed shopping results for a decade is what a crawler for an assistant finds today. What we cannot tell you, and neither can anyone else, is how a given model weighs them internally: that is not published. What is measurable is whether the data is there at all, and for most stores it is not.

Skip the placeholders

Paste your store URL. We open a real product page on your site, count these six fields, and hand back the same markup already filled in from your own page — only for the fields you are missing. Free, no email, a few seconds.

Check my product page →