Shipping Policy Schema Framework

Use conditional-logic schema generation to keep ShippingService markup aligned with the rates, destinations and delivery times you actually publish and avoid stale or invalid static templates.

When To Use It

Google Search CentralShipping Policy documentation

Place shipping information on a single page that describes the shipping policy of your business, nested under Organization via hasShippingService. It acts as the default for your products: OfferShippingDetails 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: a ShippingService requires shippingConditions, one or more ShippingConditions objects that each describe the cost and delivery time for a set of conditions.
  • Recommended on ShippingService: name (a unique name such as "Standard Shipping"), description, fulfillmentType (FulfillmentTypeDelivery, the default, or FulfillmentTypeCollectionPoint), handlingTime and validForMemberTier (the loyalty program tier the service applies to, if any).
  • Recommended on ShippingConditions: shippingDestination, shippingOrigin, shippingRate, transitTime, orderValue, weight, numItems, seasonalOverride and doesNotShip (only when applicable).
  • shippingRate: a MonetaryAmount with an ISO 4217 currency and either value (0 for free shipping) or maxValue, never both; or a ShippingRateSettings with orderPercentage or weightPercentage between 0 and 1.
  • doesNotShip: set to true when shipping from the origin to the destination isn't available, and then leave out shippingRate and transitTime.
  • DefinedRegion: addressCountry is required (ISO 3166-1 alpha-2). addressRegion takes ISO 3166-2 subdivision codes without the country prefix and is supported for the US, Australia and Japan only; postalCode is supported for Australia, Canada and the US.
  • handlingTime and transitTime are ServicePeriod objects: duration minValue, maxValue (or value) are non-negative whole numbers of days with unitCode DAY or d, and minValue must not exceed maxValue. businessDays lists the days orders are processed or in transit; cutoffTime is an ISO 8601 time such as 23:30:00-05:00.
  • Ranges: orderValue is a MonetaryAmount that requires currency; weight uses unitCode LBR or KGM; numItems takes no unitCode or H87. Minimums default to 0 and maximums to infinity.
  • seasonalOverride: an OpeningHoursSpecification with at least one of validFrom or validThrough (ISO 8601 dates) for a limited-time condition.
  • Overlaps: when several ShippingConditions apply to an order, Google uses the lowest cost, then the fastest speed.
  • Offer-level override: use shippingDetails (OfferShippingDetails) on an Offer for products with non-standard shipping; it supports a subset of the organization-level properties.
  • Precedence: Content API for Shopping, then Merchant Center or Search Console settings, then product-level markup, then organization-level markup. Every marked-up value must match the visible policy; 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",
  flatRate: 7.95,
  currency: "USD"
};

const staticTemplate = {
  "@context": "https://schema.org",
  "@type": "OnlineStore",
  "name": pageData.storeName,
  "url": pageData.homeUrl,
  "hasShippingService": {
    "@type": "ShippingService",
    "name": "Standard shipping",
    "shippingConditions": {
      "@type": "ShippingConditions",
      "shippingDestination": { "@type": "DefinedRegion", "addressCountry": pageData.country },
      "shippingRate": { "@type": "MonetaryAmount", "value": pageData.flatRate, "currency": pageData.currency },
      "transitTime": {
        "@type": "ServicePeriod",
        "duration": { "@type": "QuantitativeValue", "minValue": 3, "maxValue": 5, "unitCode": "DAY" }
      }
    }
  }
};

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 DAYS = { mon: "Monday", tue: "Tuesday", wed: "Wednesday", thu: "Thursday", fri: "Friday", sat: "Saturday", sun: "Sunday" };
const REGION_COUNTRIES = ["US", "AU", "JP"]; // addressRegion support
const POSTAL_COUNTRIES = ["AU", "CA", "US"]; // postalCode support
const WEIGHT_UNITS = { lb: "LBR", kg: "KGM" };
const ISO_TIME = /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(Z|[+-]\d{2}:\d{2})$/;

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;
}

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

function isNumber(value) {
  return typeof value === "number" && Number.isFinite(value);
}

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

// A min/max range; either end may be open, but min must not exceed max.
function toRange(range = {}) {
  const min = isNumber(range.min) && range.min >= 0 ? range.min : null;
  const max = isNumber(range.max) && range.max >= 0 ? range.max : null;
  if (min === null && max === null) return null;
  if (min !== null && max !== null && min > max) return null;
  return { minValue: min, maxValue: max };
}

function buildRegion(region) {
  const country = toCountryCode(region && region.country);
  if (!country) return null; // addressCountry is required in a DefinedRegion
  return {
    "@type": "DefinedRegion",
    addressCountry: country,
    // addressRegion: ISO 3166-2 code without the country prefix, US, AU and JP only.
    addressRegion: REGION_COUNTRIES.includes(country) ? (region.regions || []).filter((r) => /^[A-Z0-9]{2,3}$/.test(r)) : null,
    postalCode: POSTAL_COUNTRIES.includes(country) ? region.postalCodes : null // AU, CA and US only
  };
}

// handlingTime and transitTime: whole days, minValue <= maxValue, unitCode DAY.
function buildServicePeriod(period) {
  if (!period) return null;
  const [min, max] = period.days || [];
  if (toWholeNumber(min) === null || toWholeNumber(max) === null || min > max) return null;
  const cutoffTime = ISO_TIME.test(period.cutoffTime || "") ? period.cutoffTime : null;
  return {
    "@type": "ServicePeriod",
    duration: { "@type": "QuantitativeValue", minValue: min, maxValue: max, unitCode: "DAY" },
    businessDays: (period.businessDays || []).map((d) => DAYS[d] && S + DAYS[d]).filter(Boolean),
    cutoffTime
  };
}

// shippingRate: MonetaryAmount with currency and exactly one of value (0 = free) or maxValue,
// or ShippingRateSettings with orderPercentage or weightPercentage between 0 and 1.
function buildShippingRate(rate, currency) {
  if (!rate) return null;
  const hasValue = isNumber(rate.value);
  const hasMax = isNumber(rate.maxValue);
  if (hasValue || hasMax) {
    if (hasValue === hasMax || !currency) return null; // never both, currency required
    const amount = hasValue ? rate.value : rate.maxValue;
    if (amount < 0) return null;
    return { "@type": "MonetaryAmount", currency, [hasValue ? "value" : "maxValue"]: amount };
  }
  const pct = isNumber(rate.orderPercentage) ? ["orderPercentage", rate.orderPercentage]
    : isNumber(rate.weightPercentage) ? ["weightPercentage", rate.weightPercentage] : null;
  if (!pct || pct[1] < 0 || pct[1] > 1) return null;
  return { "@type": "ShippingRateSettings", [pct[0]]: pct[1] };
}

// seasonalOverride needs at least one of validFrom or validThrough.
function buildSeasonalOverride(season) {
  if (!season) return null;
  const validFrom = toIsoDate(season.from);
  const validThrough = toIsoDate(season.through);
  if (!validFrom && !validThrough) return null;
  if (validFrom && validThrough && validThrough < validFrom) return null;
  return { "@type": "OpeningHoursSpecification", validFrom, validThrough };
}

function buildShippingConditions(rule) {
  const shippingDestination = (rule.destinations || []).map(buildRegion).filter(Boolean);
  if (!shippingDestination.length) return null; // house rule: every condition names where it applies
  const currency = toCurrencyCode(rule.currency);
  const orderValue = toRange(rule.orderValue);
  const weight = toRange(rule.weight);
  const numItems = toRange(rule.numItems);
  const seasonal = rule.seasonal ? buildSeasonalOverride(rule.seasonal) : null;
  if (rule.seasonal && !seasonal) return null; // an invalid season must not widen the condition to all year

  const conditions = {
    "@type": "ShippingConditions",
    shippingDestination,
    shippingOrigin: rule.origin ? buildRegion(rule.origin) : null,
    seasonalOverride: seasonal,
    orderValue: orderValue && currency ? { "@type": "MonetaryAmount", currency, ...orderValue } : null,
    weight: weight && WEIGHT_UNITS[rule.weight.unit] ? { "@type": "QuantitativeValue", ...weight, unitCode: WEIGHT_UNITS[rule.weight.unit] } : null,
    numItems: numItems ? { "@type": "QuantitativeValue", ...numItems } : null
  };
  // No shipping to this destination: no rate and no transit time.
  if (rule.doesNotShip === true) return { ...conditions, doesNotShip: true };

  const shippingRate = buildShippingRate(rule.rate, currency);
  if (!shippingRate) return null; // house rule: a shippable condition must state its cost
  return { ...conditions, shippingRate, transitTime: buildServicePeriod(rule.transit) };
}

function buildShippingService(service) {
  // shippingConditions is required: keep the service only when at least one condition is valid.
  const shippingConditions = (service.conditions || []).map(buildShippingConditions).filter(Boolean);
  if (!shippingConditions.length) return null;
  return {
    "@type": "ShippingService",
    "@id": isAbsoluteUrl(service.id) ? service.id : null,
    name: service.name, // unique per service, for example "Standard shipping"
    description: service.description,
    fulfillmentType: S + (service.pickup ? "FulfillmentTypeCollectionPoint" : "FulfillmentTypeDelivery"),
    handlingTime: buildServicePeriod(service.handling),
    validForMemberTier: isAbsoluteUrl(service.memberTierId) ? { "@type": "MemberProgramTier", "@id": service.memberTierId } : null,
    shippingConditions
  };
}

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

  // (b) Each ShippingService requires shippingConditions; emit nothing when no service survives.
  const hasShippingService = (source.services || []).map(buildShippingService).filter(Boolean);
  if (!hasShippingService.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 in the builders above; (e) strip empty values.
  return compact({
    "@context": "https://schema.org",
    "@type": chooseOrganizationType(org),
    "@id": `${org.url}#organization`,
    name: org.name,
    url: org.url,
    hasShippingService
  });
}

// Usage:
// const source = {
//   "url": "https://www.example.com/policies/shipping",
//   "canonicalUrl": "https://www.example.com/policies/shipping",
//   "indexable": true,
//   "contentVisible": true,
//   "pageRole": "shipping_policy",
//   "organization": {
//     "kind": "online_store",
//     "name": "Trailhead Outfitters",
//     "url": "https://www.example.com/"
//   },
//   "services": [
//     {
//       "id": "https://www.example.com/policies/shipping#standard",
//       "name": "Standard shipping",
//       "description": "Ships from Massachusetts in 1 to 2 business days; free on US orders of $75 or more.",
//       "handling": {
//         "days": [
//           1,
//           2
//         ],
//         "businessDays": [
//           "mon",
//           "tue",
//           "wed",
//           "thu",
//           "fri"
//         ],
//         "cutoffTime": "14:00:00-05:00"
//       },
//       "conditions": [
//         {
//           "destinations": [
//             {
//               "country": "US"
//             }
//           ],
//           "origin": {
//             "country": "US",
//             "regions": [
//               "MA"
//             ]
//           },
//           "currency": "USD",
//           "orderValue": {
//             "min": 0,
//             "max": 74.99
//           },
//           "rate": {
//             "value": 7.95
//           },
//           "transit": {
//             "days": [
//               3,
//               5
//             ],
//             "businessDays": [
//               "mon",
//               "tue",
//               "wed",
//               "thu",
//               "fri"
//             ]
//           }
//         },
//         {
//           "destinations": [
//             {
//               "country": "US"
//             }
//           ],
//           "origin": {
//             "country": "US",
//             "regions": [
//               "MA"
//             ]
//           },
//           "currency": "USD",
//           "orderValue": {
//             "min": 75
//           },
//           "rate": {
//             "value": 0
//           },
//           "transit": {
//             "days": [
//               3,
//               5
//             ],
//             "businessDays": [
//               "mon",
//               "tue",
//               "wed",
//               "thu",
//               "fri"
//             ]
//           }
//         },
//         {
//           "destinations": [
//             {
//               "country": "AU"
//             }
//           ],
//           "currency": "AUD",
//           "weight": {
//             "min": 0,
//             "max": 10,
//             "unit": "kg"
//           },
//           "rate": {
//             "maxValue": 49.95
//           },
//           "transit": {
//             "days": [
//               6,
//               10
//             ],
//             "businessDays": [
//               "mon",
//               "tue",
//               "wed",
//               "thu",
//               "fri"
//             ]
//           }
//         },
//         {
//           "destinations": [
//             {
//               "country": "CA"
//             }
//           ],
//           "currency": "CAD",
//           "rate": {
//             "orderPercentage": 0.1
//           },
//           "seasonal": {
//             "from": "2026-11-15",
//             "through": "2026-12-24"
//           },
//           "transit": {
//             "days": [
//               4,
//               8
//             ],
//             "businessDays": [
//               "mon",
//               "tue",
//               "wed",
//               "thu",
//               "fri"
//             ]
//           }
//         },
//         {
//           "destinations": [
//             {
//               "country": "GB"
//             }
//           ],
//           "doesNotShip": true
//         }
//       ]
//     }
//   ]
// };
// const schema = buildShippingPolicySchema(source); // null when a gate or required property fails

Why Conditional Logic Is Better Than Static Templates

  • Enforces the rate rules in code: currency plus exactly one of value or maxValue, or a percentage between 0 and 1, so a pricing change cannot publish an ambiguous rate.
  • Strips shippingRate and transitTime from doesNotShip conditions and drops any condition whose seasonal window, transit days or destination is invalid.
  • Only emits addressRegion for US, AU and JP destinations and postalCode for AU, CA and US, matching Google's supported regions.
  • Models free-shipping thresholds, weight bands and seasonal surcharges as separate conditions from one rate table instead of hand-edited JSON.
  • Reads from the same rate table that renders the policy page and feeds checkout and Merchant Center, which keeps markup, visible text and higher-precedence settings in sync.

Property Reference

What Google's Merchant shipping 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 shipping policy (organization-level) properties in Google's documentation
PropertyGoogleExpectsNotes
shippingConditionsRequiredShippingConditions, can repeatThe shipping cost and/or delivery times that apply for a particular set of conditions.
shippingConditions.doesNotShipRecommendedBooleanSet to true if shipping from origin to destination isn't available.
shippingConditions.numItemsRecommendedQuantitativeValueThe range of the number of products in the order.
shippingConditions.orderValueRecommendedMonetaryAmountThe range of the cost of the order.
shippingConditions.seasonalOverrideRecommendedOpeningHoursSpecificationA limited time period for which this shipping conditions object is valid.
shippingConditions.shippingDestinationRecommendedDefinedRegion, can repeatThe shipping destination, if applicable.
shippingConditions.shippingOriginRecommendedDefinedRegionThe shipping origin, if applicable.
shippingConditions.shippingRateRecommendedMonetaryAmount or ShippingRateSettingsThe shipping cost for shipments from origin to destination.
shippingConditions.transitTimeRecommendedServicePeriodExpected transit time between leaving origin and arriving at destination.
shippingConditions.weightRecommendedQuantitativeValueThe package's weight range.
nameRecommendedTextA unique name for your shipping service, for example 'Standard Shipping'.
descriptionRecommendedTextA description of your shipping service.
fulfillmentTypeRecommendedEnumHow the product is delivered. One of: FulfillmentTypeDelivery, FulfillmentTypeCollectionPoint.
handlingTimeRecommendedServicePeriodHandling time (for example in a warehouse) after receiving an order.
handlingTime.businessDaysRecommendedEnum, can repeatDays of the week when received orders are processed. One of: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, and 1 more.
handlingTime.cutoffTimeRecommendedTimeTime after which orders received that day are not processed the same day.
handlingTime.durationRecommendedQuantitativeValueDelay between receipt of an order and the goods leaving the warehouse.
validForMemberTierRecommendedMemberProgramTierThe loyalty program and tier this shipping service is valid for, if applicable.

Explore Other Schema Framework Pages

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 Return Policy Use to publish a merchant return policy once at the organization level, with offer-level overrides. MerchantReturnPolicy Open Review Snippet Use when a page publishes genuine reviews tied to a clearly identified product, service, or entity. Review Open

Implement Shipping Policy Schema Frameworks Correctly

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