Return Policy Schema Framework

Use conditional-logic schema generation to keep MerchantReturnPolicy markup aligned with the return terms you actually publish and avoid stale or invalid static templates.

When To Use It

Google Search CentralReturn Policy documentation

Place return information on a single page that describes the return policy of your business, nested under Organization via hasMerchantReturnPolicy. It acts as the default for your products: a policy on a product's Offer overrides it, and Merchant Center or Search Console settings and the Content API for Shopping take precedence over both.

Key Implementation Documentation Highlights

  • Required option: give applicableCountry and returnPolicyCategory together, or merchantReturnLink on its own (the URL of the page that describes the policy to customers).
  • applicableCountry and returnPolicyCountry: ISO 3166-1 alpha-2 country codes, at most 50 each. applicableCountry is where the product is sold; returnPolicyCountry is where it has to be sent back to.
  • returnPolicyCategory: MerchantReturnFiniteReturnWindow, MerchantReturnNotPermitted or MerchantReturnUnlimitedWindow. A finite window requires merchantReturnDays, counted from the delivery date.
  • Recommended properties: merchantReturnDays, returnPolicyCountry, returnMethod, returnFees, returnShippingFeesAmount, returnLabelSource, itemCondition, refundType, restockingFee, the customer remorse and item defect variants, and returnPolicySeasonalOverride.
  • Enumerations: returnMethod is ReturnAtKiosk, ReturnByMail or ReturnInStore (one or more); returnFees is FreeReturn, ReturnFeesCustomerResponsibility or ReturnShippingFees; returnLabelSource is ReturnLabelCustomerResponsibility, ReturnLabelDownloadAndPrint or ReturnLabelInBox; refundType is ExchangeRefund, FullRefund or StoreCreditRefund; itemCondition is NewCondition, RefurbishedCondition, UsedCondition or DamagedCondition.
  • Fees: specify returnShippingFeesAmount (a MonetaryAmount with an ISO 4217 currency) only when returnFees is ReturnShippingFees. restockingFee as a Number is a percentage of the price paid; as a MonetaryAmount it is a fixed fee.
  • Remorse and defect returns: customerRemorseReturnFees, customerRemorseReturnLabelSource and itemDefectReturnFees, itemDefectReturnLabelSource take the same values; their ShippingFeesAmount properties are only needed when that return carries a non-zero shipping fee.
  • returnPolicySeasonalOverride: a MerchantReturnPolicySeasonalOverride for holiday or seasonal exceptions. It requires returnPolicyCategory, needs merchantReturnDays for a finite window, and takes startDate and endDate.
  • MerchantReturnNotPermitted: return fee, method and day properties are not meaningful, so leave them out.
  • Offer-level override: hasMerchantReturnPolicy on a product's Offer overrides the organization-level policy and supports a more limited set of properties (see the merchant listing documentation).
  • Precedence: Content API for Shopping, then Merchant Center or Search Console settings, then product-level markup, then organization-level markup.
  • Google expects content parity: every marked-up value must match the policy text users can read on the page. Validate with the Rich Results Test and URL Inspection after deployment.

Template Approach

const pageData = {
  storeName: "Trailhead Outfitters",
  homeUrl: "https://www.example.com/",
  country: "US",
  returnDays: 30
};

const staticTemplate = {
  "@context": "https://schema.org",
  "@type": "OnlineStore",
  "name": pageData.storeName,
  "url": pageData.homeUrl,
  "hasMerchantReturnPolicy": {
    "@type": "MerchantReturnPolicy",
    "applicableCountry": pageData.country,
    "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
    "merchantReturnDays": pageData.returnDays,
    "returnMethod": "https://schema.org/ReturnByMail",
    "returnFees": "https://schema.org/FreeReturn"
  }
};

Conditional-logic Framework

const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})?)?$/;

function isAbsoluteUrl(value) {
  return typeof value === "string" && /^https?:\/\/[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$/i.test(value);
}

// Returns the ISO 8601 string unchanged (timezone offset kept), or null if unparseable.
function toIsoDateTime(value) {
  if (typeof value !== "string") return null;
  const text = value.trim();
  const m = text.match(ISO_DATE_TIME);
  if (!m) return null;
  const [, y, mo, d, h = "00", mi = "00", s = "00"] = m;
  const date = new Date(Date.UTC(+y, +mo - 1, +d));
  if (date.getUTCMonth() !== +mo - 1 || date.getUTCDate() !== +d) return null;
  if (+h > 23 || +mi > 59 || +s > 59) return null;
  return text;
}

function toIsoDate(value) {
  const iso = toIsoDateTime(value);
  return iso ? iso.slice(0, 10) : null;
}

// Drops null, undefined, "", [] and {} recursively.
function compact(value) {
  if (Array.isArray(value)) {
    const items = value.map(compact).filter((v) => v !== undefined);
    return items.length ? items : undefined;
  }
  if (value && typeof value === "object") {
    const out = {};
    for (const [key, v] of Object.entries(value)) {
      const c = compact(v);
      if (c !== undefined) out[key] = c;
    }
    return Object.keys(out).length ? out : undefined;
  }
  return value === null || value === undefined || value === "" ? undefined : value;
}

function passesPageGates(source) {
  if (source.indexable === false) return false;
  if (source.canonicalUrl && source.url && source.canonicalUrl !== source.url) return false;
  return source.contentVisible !== false;
}


const S = "https://schema.org/";
const ORGANIZATION_TYPES = { online_store: "OnlineStore", online_business: "OnlineBusiness", corporation: "Corporation" };
const RETURN_CATEGORY = {
  finite: S + "MerchantReturnFiniteReturnWindow",
  not_permitted: S + "MerchantReturnNotPermitted",
  unlimited: S + "MerchantReturnUnlimitedWindow"
};
const RETURN_FEES = { free: S + "FreeReturn", customer_pays: S + "ReturnFeesCustomerResponsibility", shipping_fee: S + "ReturnShippingFees" };
const RETURN_METHOD = { kiosk: S + "ReturnAtKiosk", mail: S + "ReturnByMail", in_store: S + "ReturnInStore" };
const RETURN_LABEL = { customer: S + "ReturnLabelCustomerResponsibility", download: S + "ReturnLabelDownloadAndPrint", in_box: S + "ReturnLabelInBox" };
const REFUND_TYPE = { exchange: S + "ExchangeRefund", full: S + "FullRefund", store_credit: S + "StoreCreditRefund" };
const ITEM_CONDITION = { new: S + "NewCondition", refurbished: S + "RefurbishedCondition", used: S + "UsedCondition", damaged: S + "DamagedCondition" };

function chooseOrganizationType(org) {
  return ORGANIZATION_TYPES[org.kind] || "Organization";
}

function toCountryCode(value) {
  const code = String(value || "").toUpperCase();
  return /^[A-Z]{2}$/.test(code) ? code : null;
}

// ISO 3166-1 alpha-2 codes, at most 50; null when any code is invalid or the list is too long.
function toCountryList(values) {
  const codes = (values || []).map(toCountryCode);
  if (codes.includes(null) || codes.length > 50) return null;
  return [...new Set(codes)];
}

function toWholeNumber(value) {
  return Number.isInteger(value) && value >= 0 ? value : null;
}

function mapEnum(keys, table) {
  return (keys || []).map((k) => table[k]).filter(Boolean);
}

function buildMoney(amount) {
  const currency = String((amount && amount.currency) || "").toUpperCase();
  if (!amount || typeof amount.value !== "number" || amount.value < 0 || !/^[A-Z]{3}$/.test(currency)) return null;
  return { "@type": "MonetaryAmount", value: amount.value, currency };
}

// restockingFee: a Number is a percentage of the price paid, a MonetaryAmount is a fixed fee.
function buildRestockingFee(fee) {
  if (!fee) return null;
  if (typeof fee.percent === "number") return fee.percent >= 0 && fee.percent <= 100 ? fee.percent : null;
  return buildMoney(fee);
}

// Fee type and label source; the shipping fee amount only (and always) with ReturnShippingFees.
function buildFees(group) {
  if (!group) return {};
  const amount = buildMoney(group.shippingFee);
  let fees = RETURN_FEES[group.fees] || null;
  if (fees === RETURN_FEES.shipping_fee && !amount) fees = null;
  return { fees, label: RETURN_LABEL[group.label] || null, amount: fees === RETURN_FEES.shipping_fee ? amount : null };
}

function buildSeasonalOverride(season) {
  const category = RETURN_CATEGORY[season.category];
  if (!category) return null; // returnPolicyCategory is required in an override
  const days = toWholeNumber(season.days);
  if (category === RETURN_CATEGORY.finite && days === null) return null;
  const startDate = toIsoDate(season.start);
  const endDate = toIsoDate(season.end);
  if (!startDate || !endDate || endDate < startDate) return null; // a dated window, end on or after start
  return {
    "@type": "MerchantReturnPolicySeasonalOverride",
    startDate,
    endDate,
    returnPolicyCategory: category,
    merchantReturnDays: category === RETURN_CATEGORY.finite ? days : null
  };
}

function buildReturnPolicy(policy) {
  const link = isAbsoluteUrl(policy.link) ? policy.link : null;
  const countries = toCountryList(policy.countries);
  const category = RETURN_CATEGORY[policy.category] || null;
  if (countries === null) return null; // invalid code or more than 50 countries

  // Required option: applicableCountry + returnPolicyCategory, or merchantReturnLink alone.
  if (!(countries.length && category)) {
    return link ? { "@type": "MerchantReturnPolicy", merchantReturnLink: link } : null;
  }
  const days = toWholeNumber(policy.days);
  if (category === RETURN_CATEGORY.finite && days === null) return null; // finite window needs merchantReturnDays

  const base = { "@type": "MerchantReturnPolicy", applicableCountry: countries, returnPolicyCategory: category, merchantReturnLink: link };
  // No returns: fees, methods and days are not meaningful.
  if (category === RETURN_CATEGORY.not_permitted) return base;

  const main = buildFees(policy);
  const remorse = buildFees(policy.customerRemorse);
  const defect = buildFees(policy.itemDefect);
  return {
    ...base,
    merchantReturnDays: category === RETURN_CATEGORY.finite ? days : null,
    returnPolicyCountry: toCountryList(policy.returnToCountries),
    returnMethod: mapEnum(policy.methods, RETURN_METHOD),
    returnFees: main.fees,
    returnShippingFeesAmount: main.amount,
    returnLabelSource: main.label,
    refundType: mapEnum(policy.refundTypes, REFUND_TYPE),
    itemCondition: mapEnum(policy.conditions, ITEM_CONDITION),
    restockingFee: buildRestockingFee(policy.restockingFee),
    customerRemorseReturnFees: remorse.fees,
    customerRemorseReturnShippingFeesAmount: remorse.amount,
    customerRemorseReturnLabelSource: remorse.label,
    itemDefectReturnFees: defect.fees,
    itemDefectReturnShippingFeesAmount: defect.amount,
    itemDefectReturnLabelSource: defect.label,
    returnPolicySeasonalOverride: (policy.seasonalOverrides || []).map(buildSeasonalOverride)
  };
}

function buildReturnPolicySchema(source) {
  // (a) Page gates: indexable, self-canonical, visible policy text, on the page that describes the return policy.
  if (!passesPageGates(source) || source.pageRole !== "return_policy") return null;

  // (b) At least one policy must pass Google's required option (country + category, or merchantReturnLink).
  const hasMerchantReturnPolicy = (source.policies || []).map(buildReturnPolicy).filter(Boolean);
  if (!hasMerchantReturnPolicy.length) return null;

  // House rule: nest under the Organization, identified by name, home URL and the same @id as the home page markup.
  const org = source.organization || {};
  if (!org.name || !isAbsoluteUrl(org.url)) return null;

  // (c) and (d) applied inside buildReturnPolicy; (e) strip empty values.
  return compact({
    "@context": "https://schema.org",
    "@type": chooseOrganizationType(org),
    "@id": `${org.url}#organization`,
    name: org.name,
    url: org.url,
    hasMerchantReturnPolicy
  });
}

// Usage:
// const source = {
//   "url": "https://www.example.com/policies/returns",
//   "canonicalUrl": "https://www.example.com/policies/returns",
//   "indexable": true,
//   "contentVisible": true,
//   "pageRole": "return_policy",
//   "organization": {
//     "kind": "online_store",
//     "name": "Trailhead Outfitters",
//     "url": "https://www.example.com/"
//   },
//   "policies": [
//     {
//       "countries": [
//         "US",
//         "CA"
//       ],
//       "returnToCountries": [
//         "US"
//       ],
//       "category": "finite",
//       "days": 30,
//       "link": "https://www.example.com/policies/returns",
//       "methods": [
//         "mail",
//         "in_store"
//       ],
//       "fees": "shipping_fee",
//       "shippingFee": {
//         "value": 6.95,
//         "currency": "USD"
//       },
//       "label": "download",
//       "refundTypes": [
//         "full",
//         "store_credit"
//       ],
//       "conditions": [
//         "new"
//       ],
//       "restockingFee": {
//         "percent": 0
//       },
//       "customerRemorse": {
//         "fees": "shipping_fee",
//         "shippingFee": {
//           "value": 6.95,
//           "currency": "USD"
//         },
//         "label": "download"
//       },
//       "itemDefect": {
//         "fees": "free",
//         "label": "in_box"
//       },
//       "seasonalOverrides": [
//         {
//           "category": "finite",
//           "days": 60,
//           "start": "2026-11-20",
//           "end": "2026-12-31"
//         }
//       ]
//     }
//   ]
// };
// const schema = buildReturnPolicySchema(source); // null when a gate or required property fails

Why Conditional Logic Is Better Than Static Templates

  • Enforces the required option in code: applicableCountry plus returnPolicyCategory, or merchantReturnLink alone, and never a policy with neither.
  • Drops fee, method and day properties when returns are not permitted, and blocks a finite window that has no merchantReturnDays.
  • Keeps returnShippingFeesAmount tied to ReturnShippingFees, so a fee change in the policy config cannot leave a stale amount behind.
  • Emits seasonal overrides only with a valid dated window, so a lapsed holiday policy is never published as all-year.
  • Reads from the same policy config that renders the page and feeds Merchant Center, which keeps markup, visible text and higher-precedence settings in sync.

Property Reference

What Google's Merchant return policy (organization-level) documentation requires and recommends, property by property. These are the same rules the SchemaCDN validator checks. Each name links to its schema.org definition.

Merchant return policy (organization-level) properties in Google's documentation
PropertyGoogleExpectsNotes
applicableCountryRequiredText, can repeatThe country code(s) the return policy applies to (where the product is sold and returned from)
returnPolicyCategoryRequiredEnumThe type of return policy. One of: MerchantReturnFiniteReturnWindow, MerchantReturnNotPermitted, MerchantReturnUnlimitedWindow.
merchantReturnLinkRequiredURLThe URL of a web page that describes the return policy to your customers.
merchantReturnDaysRecommendedIntegerThe number of days from the delivery date that a product can be returned.
returnPolicyCountryRecommendedText, can repeatThe country where the product has to be sent to for returns.
returnMethodRecommendedEnum, can repeatThe type of return method offered; one or more values. One of: ReturnAtKiosk, ReturnByMail, ReturnInStore.
returnFeesRecommendedEnumThe type of return fees. One of: FreeReturn, ReturnFeesCustomerResponsibility, ReturnShippingFees.
returnShippingFeesAmountRecommendedMonetaryAmountThe cost of shipping for returning a product.
returnShippingFeesAmount.currencyRecommendedTextISO 4217 currency code.
returnShippingFeesAmount.valueRecommendedNumberThe amount.
returnLabelSourceRecommendedEnumHow the customer obtains a return shipping label. One of: ReturnLabelCustomerResponsibility, ReturnLabelDownloadAndPrint, ReturnLabelInBox.
itemConditionRecommendedEnum, can repeatAcceptable condition(s) of returned items. One of: DamagedCondition, NewCondition, RefurbishedCondition, UsedCondition.
refundTypeRecommendedEnum, can repeatThe type of refund available. One of: ExchangeRefund, FullRefund, StoreCreditRefund.
restockingFeeRecommendedMonetaryAmount or NumberThe restocking fee charged for returns.
customerRemorseReturnFeesRecommendedEnumReturn fee type if the product is returned due to customer remorse. One of: FreeReturn, ReturnFeesCustomerResponsibility, ReturnShippingFees.
customerRemorseReturnLabelSourceRecommendedEnumReturn label source for customer remorse returns. One of: ReturnLabelCustomerResponsibility, ReturnLabelDownloadAndPrint, ReturnLabelInBox.
customerRemorseReturnShippingFeesAmountRecommendedMonetaryAmountShipping fee for customer remorse returns.
customerRemorseReturnShippingFeesAmount.currencyRecommendedTextISO 4217 currency code.
customerRemorseReturnShippingFeesAmount.valueRecommendedNumberThe amount.
itemDefectReturnFeesRecommendedEnumReturn fee type for defect products. One of: FreeReturn, ReturnFeesCustomerResponsibility, ReturnShippingFees.
itemDefectReturnLabelSourceRecommendedEnumReturn label source for defect product returns. One of: ReturnLabelCustomerResponsibility, ReturnLabelDownloadAndPrint, ReturnLabelInBox.
itemDefectReturnShippingFeesAmountRecommendedMonetaryAmountShipping fee for defect product returns.
itemDefectReturnShippingFeesAmount.currencyRecommendedTextISO 4217 currency code.
itemDefectReturnShippingFeesAmount.valueRecommendedNumberThe amount.
returnPolicySeasonalOverrideRecommendedMerchantReturnPolicySeasonalOverride, can repeatA seasonal override of the return policy.
returnPolicySeasonalOverride.returnPolicyCategoryRequiredEnumThe type of return policy for the season. One of: MerchantReturnFiniteReturnWindow, MerchantReturnNotPermitted, MerchantReturnUnlimitedWindow.
returnPolicySeasonalOverride.merchantReturnDaysRecommendedIntegerNumber of return days during the season.
returnPolicySeasonalOverride.startDateRecommendedDate or DateTimeStart of the seasonal override.
returnPolicySeasonalOverride.endDateRecommendedDate or DateTimeEnd of the seasonal override.

Explore Other Schema Framework Pages

Review Snippet Use when a page publishes genuine reviews tied to a clearly identified product, service, or entity. Review Open Shipping Policy Use to publish shipping rates, delivery times and destinations once at the organization level. ShippingService Open Site Name Use on the home page so Google shows the preferred site name in search results. WebSite Open Retired Sitelinks Search Box Use on sites with a functioning internal search endpoint that can accept URL query parameters. WebSite Open Software App (Beta) Use for software product pages that describe platform support and application category clearly. SoftwareApplication Open Limited Speakable Use for news-style pages where specific text segments are appropriate for text-to-speech playback. SpeakableSpecification Open Subscription and Paywalled Content Use for pages with restricted sections so Google can distinguish free previews from protected content. CreativeWork Open Limited Vacation Rental Use for rental property detail pages where booking-relevant amenities and location signals are explicit. VacationRental Open Retired Vehicle Listing Use for inventory pages selling vehicles with standardized listing fields and offer information. Car Open Video Use for pages where a primary video is embedded and key media metadata is visible on-page. VideoObject Open Article Use for editorial pages where the visible content is a complete article with a clear byline and publication timestamps. Article Open Limited Book Actions Use when your page helps users read, preview, or purchase books through clearly described actions. Book Open Breadcrumb Use on pages that belong to a clear hierarchy and display navigational trail links users can follow. BreadcrumbList Open Carousel Use when a page lists multiple related entities in a scannable sequence that can be surfaced as cards. ItemList Open Course Use for educational offerings where curriculum details, provider identity, and delivery context are explicit. Course Open Dataset Use for dataset landing pages so datasets are discoverable in Google Dataset Search with license, creator and download details. Dataset Open Discussion Forum Use for forum and community threads where a primary post and subsequent replies are publicly visible. DiscussionForumPosting Open Limited Education Q&A Use for educational question-answer pages where a single canonical answer and supporting context are visible. Quiz Open Employer Aggregate Rating Use on pages that publish employer rating aggregates from a clear and policy-compliant review source. EmployerAggregateRating Open Retired Estimated Salary Use for compensation insight pages that present transparent salary ranges for specific occupations and locations. Occupation Open Event Use for upcoming events with a real-world or virtual occurrence, schedule, and location context. Event Open Retired Fact Check Use for fact-checking pages that evaluate a specific claim with a transparent rating methodology. ClaimReview Open Retired Home Activities Use for virtual classes and activities where participation details and timing are clearly listed. Event Open Retired How-To Use for instructional content that teaches a process through ordered steps and actionable detail. HowTo Open Image Metadata Use on image pages and image hosts that expose ownership and licensing information to search engines. ImageObject Open Job Posting Use for active job listings that include role details, hiring entity, and location/salary context. JobPosting Open Retired Learning Video Use for educational videos where the learning objective and pedagogical structure are explicit. LearningResource Open Local Business Use for physical businesses serving local customers with publicly verifiable contact and location details. LocalBusiness Open Loyalty Program Use to publish a loyalty program and its tiers once at the organization level, so member prices and member shipping can reference them. MemberProgram Open Math Solvers Use for pages that solve mathematical expressions and present step-by-step computational guidance. MathSolver Open Merchant Listing Use on pages where a product can be bought, with price, availability, shipping and return details. Product Open Limited Movie Use for movie detail pages with canonical film information and transparent metadata attribution. Movie Open Organization Use for official organization pages to clarify brand identity and authoritative entity signals. Organization Open Retired Practice Problems Use for educational practice content that presents solvable problems and answer validation structure. Quiz Open Product Snippet Use for product pages where pricing, availability, and product identity are visible to users. Product Open Product Variants Use when one product comes in variants such as size or color, grouped with ProductGroup. ProductGroup Open Profile Page Use for creator or contributor profile pages where identity attributes and ownership are explicit. ProfilePage Open Q&A Use for pages where users submit questions and community or editorial answers are displayed. QAPage Open Recipe Use for recipe pages with complete ingredients, instructions, and preparation details. Recipe Open

Implement Return Policy Schema Frameworks Correctly

Get implementation support for conditional schema architecture, validation, and deployment governance.