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 documentationPlace 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" }
}
}
}
};
{
"@context": "https://schema.org",
"@type": "OnlineStore",
"name": "Trailhead Outfitters",
"url": "https://www.example.com/",
"hasShippingService": {
"@type": "ShippingService",
"name": "Standard shipping",
"shippingConditions": {
"@type": "ShippingConditions",
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
},
"shippingRate": {
"@type": "MonetaryAmount",
"value": 7.95,
"currency": "USD"
},
"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
import re
from datetime import date, datetime, timezone
ISO_DATE_TIME = re.compile(r"^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})?)?$")
ABSOLUTE_URL = re.compile(r"^https?://[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$", re.I)
def is_absolute_url(value):
return isinstance(value, str) and bool(ABSOLUTE_URL.match(value))
# Returns the ISO 8601 string unchanged (timezone offset kept), or None if unparseable.
def to_iso_date_time(value):
if not isinstance(value, str):
return None
text = value.strip()
m = ISO_DATE_TIME.match(text)
if not m:
return None
y, mo, d = int(m[1]), int(m[2]), int(m[3])
try:
date(y, mo, d)
except ValueError:
return None
if int(m[4] or 0) > 23 or int(m[5] or 0) > 59 or int(m[6] or 0) > 59:
return None
return text
def to_iso_date(value):
iso = to_iso_date_time(value)
return iso[:10] if iso else None
# Drops None, "", [] and {} recursively.
def compact(value):
if isinstance(value, list):
items = [c for c in (compact(v) for v in value) if c is not None]
return items or None
if isinstance(value, dict):
out = {k: c for k, c in ((k, compact(v)) for k, v in value.items()) if c is not None}
return out or None
return None if value is None or value == "" else value
def passes_page_gates(source):
if source.get("indexable") is False:
return False
if source.get("canonicalUrl") and source.get("url") and source["canonicalUrl"] != source["url"]:
return False
return source.get("contentVisible") is not False
S = "https://schema.org/"
ORGANIZATION_TYPES = {"online_store": "OnlineStore", "online_business": "OnlineBusiness", "corporation": "Corporation"}
DAYS = {"mon": "Monday", "tue": "Tuesday", "wed": "Wednesday", "thu": "Thursday", "fri": "Friday", "sat": "Saturday", "sun": "Sunday"}
REGION_COUNTRIES = ("US", "AU", "JP") # addressRegion support
POSTAL_COUNTRIES = ("AU", "CA", "US") # postalCode support
WEIGHT_UNITS = {"lb": "LBR", "kg": "KGM"}
ISO_TIME = re.compile(r"^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(Z|[+-]\d{2}:\d{2})$")
def choose_organization_type(org):
return ORGANIZATION_TYPES.get(org.get("kind"), "Organization")
def to_country_code(value):
code = str(value or "").upper()
return code if re.fullmatch(r"[A-Z]{2}", code) else None
def to_currency_code(value):
code = str(value or "").upper()
return code if re.fullmatch(r"[A-Z]{3}", code) else None
def is_number(value):
return isinstance(value, (int, float)) and not isinstance(value, bool)
def to_whole_number(value):
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
# A min/max range; either end may be open, but min must not exceed max.
def to_range(rng):
rng = rng or {}
low = rng.get("min") if is_number(rng.get("min")) and rng["min"] >= 0 else None
high = rng.get("max") if is_number(rng.get("max")) and rng["max"] >= 0 else None
if low is None and high is None:
return None
if low is not None and high is not None and low > high:
return None
return {"minValue": low, "maxValue": high}
def build_region(region):
country = to_country_code((region or {}).get("country"))
if not country:
return None # 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": [r for r in region.get("regions") or [] if re.fullmatch(r"[A-Z0-9]{2,3}", r)] if country in REGION_COUNTRIES else None,
"postalCode": region.get("postalCodes") if country in POSTAL_COUNTRIES else None, # AU, CA and US only
}
# handlingTime and transitTime: whole days, minValue <= maxValue, unitCode DAY.
def build_service_period(period):
if not period:
return None
low, high = (list(period.get("days") or []) + [None, None])[:2]
if to_whole_number(low) is None or to_whole_number(high) is None or low > high:
return None
cutoff = period.get("cutoffTime") if ISO_TIME.match(period.get("cutoffTime") or "") else None
return {
"@type": "ServicePeriod",
"duration": {"@type": "QuantitativeValue", "minValue": low, "maxValue": high, "unitCode": "DAY"},
"businessDays": [S + DAYS[d] for d in period.get("businessDays") or [] if d in DAYS],
"cutoffTime": cutoff,
}
# shippingRate: MonetaryAmount with currency and exactly one of value (0 = free) or maxValue,
# or ShippingRateSettings with orderPercentage or weightPercentage between 0 and 1.
def build_shipping_rate(rate, currency):
if not rate:
return None
has_value, has_max = is_number(rate.get("value")), is_number(rate.get("maxValue"))
if has_value or has_max:
if has_value == has_max or not currency:
return None # never both, currency required
amount = rate["value"] if has_value else rate["maxValue"]
if amount < 0:
return None
return {"@type": "MonetaryAmount", "currency": currency, ("value" if has_value else "maxValue"): amount}
if is_number(rate.get("orderPercentage")):
pct = ("orderPercentage", rate["orderPercentage"])
elif is_number(rate.get("weightPercentage")):
pct = ("weightPercentage", rate["weightPercentage"])
else:
return None
if pct[1] < 0 or pct[1] > 1:
return None
return {"@type": "ShippingRateSettings", pct[0]: pct[1]}
# seasonalOverride needs at least one of validFrom or validThrough.
def build_seasonal_override(season):
if not season:
return None
valid_from, valid_through = to_iso_date(season.get("from")), to_iso_date(season.get("through"))
if not valid_from and not valid_through:
return None
if valid_from and valid_through and valid_through < valid_from:
return None
return {"@type": "OpeningHoursSpecification", "validFrom": valid_from, "validThrough": valid_through}
def build_shipping_conditions(rule):
destination = [r for r in (build_region(d) for d in rule.get("destinations") or []) if r]
if not destination:
return None # house rule: every condition names where it applies
currency = to_currency_code(rule.get("currency"))
order_value, weight, num_items = to_range(rule.get("orderValue")), to_range(rule.get("weight")), to_range(rule.get("numItems"))
seasonal = build_seasonal_override(rule.get("seasonal")) if rule.get("seasonal") else None
if rule.get("seasonal") and not seasonal:
return None # an invalid season must not widen the condition to all year
weight_unit = WEIGHT_UNITS.get((rule.get("weight") or {}).get("unit"))
conditions = {
"@type": "ShippingConditions",
"shippingDestination": destination,
"shippingOrigin": build_region(rule["origin"]) if rule.get("origin") else None,
"seasonalOverride": seasonal,
"orderValue": {"@type": "MonetaryAmount", "currency": currency, **order_value} if order_value and currency else None,
"weight": {"@type": "QuantitativeValue", **weight, "unitCode": weight_unit} if weight and weight_unit else None,
"numItems": {"@type": "QuantitativeValue", **num_items} if num_items else None,
}
# No shipping to this destination: no rate and no transit time.
if rule.get("doesNotShip") is True:
return {**conditions, "doesNotShip": True}
shipping_rate = build_shipping_rate(rule.get("rate"), currency)
if not shipping_rate:
return None # house rule: a shippable condition must state its cost
return {**conditions, "shippingRate": shipping_rate, "transitTime": build_service_period(rule.get("transit"))}
def build_shipping_service(service):
# shippingConditions is required: keep the service only when at least one condition is valid.
conditions = [c for c in (build_shipping_conditions(r) for r in service.get("conditions") or []) if c]
if not conditions:
return None
tier = service.get("memberTierId")
return {
"@type": "ShippingService",
"@id": service.get("id") if is_absolute_url(service.get("id")) else None,
"name": service.get("name"), # unique per service, for example "Standard shipping"
"description": service.get("description"),
"fulfillmentType": S + ("FulfillmentTypeCollectionPoint" if service.get("pickup") else "FulfillmentTypeDelivery"),
"handlingTime": build_service_period(service.get("handling")),
"validForMemberTier": {"@type": "MemberProgramTier", "@id": tier} if is_absolute_url(tier) else None,
"shippingConditions": conditions,
}
def build_shipping_policy_schema(source):
# (a) Page gates: indexable, self-canonical, visible policy text, on the page that describes the shipping policy.
if not passes_page_gates(source) or source.get("pageRole") != "shipping_policy":
return None
# (b) Each ShippingService requires shippingConditions; emit nothing when no service survives.
services = [s for s in (build_shipping_service(s) for s in source.get("services") or []) if s]
if not services:
return None
# House rule: nest under the Organization, identified by name, home URL and the same @id as the home page markup.
org = source.get("organization") or {}
if not org.get("name") or not is_absolute_url(org.get("url")):
return None
# (c) and (d) applied in the builders above; (e) strip empty values.
return compact({
"@context": "https://schema.org",
"@type": choose_organization_type(org),
"@id": f"{org['url']}#organization",
"name": org["name"],
"url": org["url"],
"hasShippingService": services,
})
{
"@context": "https://schema.org",
"@type": "OnlineStore",
"@id": "https://www.example.com/#organization",
"name": "Trailhead Outfitters",
"url": "https://www.example.com/",
"hasShippingService": [
{
"@type": "ShippingService",
"@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.",
"fulfillmentType": "https://schema.org/FulfillmentTypeDelivery",
"handlingTime": {
"@type": "ServicePeriod",
"duration": {
"@type": "QuantitativeValue",
"minValue": 1,
"maxValue": 2,
"unitCode": "DAY"
},
"businessDays": [
"https://schema.org/Monday",
"https://schema.org/Tuesday",
"https://schema.org/Wednesday",
"https://schema.org/Thursday",
"https://schema.org/Friday"
],
"cutoffTime": "14:00:00-05:00"
},
"shippingConditions": [
{
"@type": "ShippingConditions",
"shippingDestination": [
{
"@type": "DefinedRegion",
"addressCountry": "US"
}
],
"shippingOrigin": {
"@type": "DefinedRegion",
"addressCountry": "US",
"addressRegion": [
"MA"
]
},
"orderValue": {
"@type": "MonetaryAmount",
"currency": "USD",
"minValue": 0,
"maxValue": 74.99
},
"shippingRate": {
"@type": "MonetaryAmount",
"currency": "USD",
"value": 7.95
},
"transitTime": {
"@type": "ServicePeriod",
"duration": {
"@type": "QuantitativeValue",
"minValue": 3,
"maxValue": 5,
"unitCode": "DAY"
},
"businessDays": [
"https://schema.org/Monday",
"https://schema.org/Tuesday",
"https://schema.org/Wednesday",
"https://schema.org/Thursday",
"https://schema.org/Friday"
]
}
},
{
"@type": "ShippingConditions",
"shippingDestination": [
{
"@type": "DefinedRegion",
"addressCountry": "US"
}
],
"shippingOrigin": {
"@type": "DefinedRegion",
"addressCountry": "US",
"addressRegion": [
"MA"
]
},
"orderValue": {
"@type": "MonetaryAmount",
"currency": "USD",
"minValue": 75
},
"shippingRate": {
"@type": "MonetaryAmount",
"currency": "USD",
"value": 0
},
"transitTime": {
"@type": "ServicePeriod",
"duration": {
"@type": "QuantitativeValue",
"minValue": 3,
"maxValue": 5,
"unitCode": "DAY"
},
"businessDays": [
"https://schema.org/Monday",
"https://schema.org/Tuesday",
"https://schema.org/Wednesday",
"https://schema.org/Thursday",
"https://schema.org/Friday"
]
}
},
{
"@type": "ShippingConditions",
"shippingDestination": [
{
"@type": "DefinedRegion",
"addressCountry": "AU"
}
],
"weight": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 10,
"unitCode": "KGM"
},
"shippingRate": {
"@type": "MonetaryAmount",
"currency": "AUD",
"maxValue": 49.95
},
"transitTime": {
"@type": "ServicePeriod",
"duration": {
"@type": "QuantitativeValue",
"minValue": 6,
"maxValue": 10,
"unitCode": "DAY"
},
"businessDays": [
"https://schema.org/Monday",
"https://schema.org/Tuesday",
"https://schema.org/Wednesday",
"https://schema.org/Thursday",
"https://schema.org/Friday"
]
}
},
{
"@type": "ShippingConditions",
"shippingDestination": [
{
"@type": "DefinedRegion",
"addressCountry": "CA"
}
],
"seasonalOverride": {
"@type": "OpeningHoursSpecification",
"validFrom": "2026-11-15",
"validThrough": "2026-12-24"
},
"shippingRate": {
"@type": "ShippingRateSettings",
"orderPercentage": 0.1
},
"transitTime": {
"@type": "ServicePeriod",
"duration": {
"@type": "QuantitativeValue",
"minValue": 4,
"maxValue": 8,
"unitCode": "DAY"
},
"businessDays": [
"https://schema.org/Monday",
"https://schema.org/Tuesday",
"https://schema.org/Wednesday",
"https://schema.org/Thursday",
"https://schema.org/Friday"
]
}
},
{
"@type": "ShippingConditions",
"shippingDestination": [
{
"@type": "DefinedRegion",
"addressCountry": "GB"
}
],
"doesNotShip": true
}
]
}
]
}
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.
| Property | Expects | Notes | |
|---|---|---|---|
| shippingConditions | Required | ShippingConditions, can repeat | The shipping cost and/or delivery times that apply for a particular set of conditions. |
| shippingConditions.doesNotShip | Recommended | Boolean | Set to true if shipping from origin to destination isn't available. |
| shippingConditions.numItems | Recommended | QuantitativeValue | The range of the number of products in the order. |
| shippingConditions.orderValue | Recommended | MonetaryAmount | The range of the cost of the order. |
| shippingConditions.seasonalOverride | Recommended | OpeningHoursSpecification | A limited time period for which this shipping conditions object is valid. |
| shippingConditions.shippingDestination | Recommended | DefinedRegion, can repeat | The shipping destination, if applicable. |
| shippingConditions.shippingOrigin | Recommended | DefinedRegion | The shipping origin, if applicable. |
| shippingConditions.shippingRate | Recommended | MonetaryAmount or ShippingRateSettings | The shipping cost for shipments from origin to destination. |
| shippingConditions.transitTime | Recommended | ServicePeriod | Expected transit time between leaving origin and arriving at destination. |
| shippingConditions.weight | Recommended | QuantitativeValue | The package's weight range. |
| name | Recommended | Text | A unique name for your shipping service, for example 'Standard Shipping'. |
| description | Recommended | Text | A description of your shipping service. |
| fulfillmentType | Recommended | Enum | How the product is delivered. One of: FulfillmentTypeDelivery, FulfillmentTypeCollectionPoint. |
| handlingTime | Recommended | ServicePeriod | Handling time (for example in a warehouse) after receiving an order. |
| handlingTime.businessDays | Recommended | Enum, can repeat | Days of the week when received orders are processed. One of: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, and 1 more. |
| handlingTime.cutoffTime | Recommended | Time | Time after which orders received that day are not processed the same day. |
| handlingTime.duration | Recommended | QuantitativeValue | Delay between receipt of an order and the goods leaving the warehouse. |
| validForMemberTier | Recommended | MemberProgramTier | The loyalty program and tier this shipping service is valid for, if applicable. |