Loyalty Program Schema Framework

Use conditional-logic schema generation to keep MemberProgram markup aligned with the loyalty tiers and benefits you actually publish and avoid stale or invalid static templates.

When To Use It

Google Search CentralLoyalty Program documentation

Nest MemberProgram under Organization via hasMemberProgram, on the page that carries your business's administrative details and policies. Define each tier once with an @id, then reference it from product offers and shipping services through validForMemberTier to show member prices, loyalty points and member shipping.

Key Implementation Documentation Highlights

  • Availability: loyalty program markup is available in Australia, Brazil, Canada, France, Germany, Mexico, the UK and the US, on desktop and mobile.
  • MemberProgram required properties: name, description (the primary benefits for members) and hasTiers (at least one MemberProgramTier). Recommended: url, the page where a shopper signs up; give a single URL, not several.
  • MemberProgramTier required properties: name and hasTierBenefit, one or both of https://schema.org/TierBenefitLoyaltyPoints and https://schema.org/TierBenefitLoyaltyPrice. Recommended: hasTierRequirement, membershipPointsEarned, url (the sign up page for that tier).
  • membershipPointsEarned (a QuantitativeValue: points earned per unit of currency spent) is listed as recommended, but it is required on any tier whose hasTierBenefit includes TierBenefitLoyaltyPoints.
  • hasTierRequirement variants: CreditCard with name (a co-branded card); MonetaryAmount with value and currency (a minimum spend); UnitPriceSpecification with price, priceCurrency, billingDuration, billingIncrement and unitCode (a paid membership); or free-form Text. Without a requirement, anyone can join the tier for free.
  • Give every tier an @id: offers and shipping services point at a tier with {"@id": ...} rather than repeating it.
  • Member prices: add a UnitPriceSpecification to the Offer's priceSpecification with validForMemberTier and, for points, membershipPointsEarned (a whole number, only together with validForMemberTier). validForMemberTier cannot be combined with priceType; the active non-member price has neither. See the merchant listing framework.
  • Member shipping: a ShippingService under Organization.hasShippingService can carry validForMemberTier for shipping rates that only apply to a tier. See the shipping policy framework.
  • Google expects content parity: tier names, benefits, requirements and point rates must match the program terms users can read. Validate with the Rich Results Test and URL Inspection after deployment.

Template Approach

const pageData = {
  storeName: "Trailhead Outfitters",
  homeUrl: "https://www.example.com/",
  programName: "Basecamp Rewards",
  programDescription: "Earn points on every order and unlock member prices.",
  pointsPerDollar: 1
};

const staticTemplate = {
  "@context": "https://schema.org",
  "@type": "OnlineStore",
  "name": pageData.storeName,
  "url": pageData.homeUrl,
  "hasMemberProgram": {
    "@type": "MemberProgram",
    "name": pageData.programName,
    "description": pageData.programDescription,
    "hasTiers": [
      {
        "@type": "MemberProgramTier",
        "@id": pageData.homeUrl + "#tier-insider",
        "name": "Insider",
        "hasTierBenefit": [
          "https://schema.org/TierBenefitLoyaltyPoints",
          "https://schema.org/TierBenefitLoyaltyPrice"
        ],
        "membershipPointsEarned": {
          "@type": "QuantitativeValue",
          "value": pageData.pointsPerDollar
        }
      }
    ]
  }
};

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 TIER_BENEFIT = { points: S + "TierBenefitLoyaltyPoints", member_price: S + "TierBenefitLoyaltyPrice" };

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

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

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

// Stable fragment id per tier, reused by Offer and OfferShippingDetails validForMemberTier.
function tierId(orgUrl, key) {
  return /^[a-z0-9-]+$/.test(String(key || "")) ? `${orgUrl}#tier-${key}` : null;
}

// hasTierRequirement variants. undefined = no requirement (free to join), null = invalid.
function buildTierRequirement(req) {
  if (!req || req.type === "none") return undefined;
  if (req.type === "credit_card") {
    return req.name ? { "@type": "CreditCard", name: req.name } : null;
  }
  if (req.type === "min_spend") {
    const currency = toCurrencyCode(req.currency);
    return isPositiveNumber(req.value) && currency ? { "@type": "MonetaryAmount", value: req.value, currency } : null;
  }
  if (req.type === "paid_membership") {
    const priceCurrency = toCurrencyCode(req.currency);
    const ok = isPositiveNumber(req.price) && priceCurrency && isPositiveNumber(req.billingDuration) &&
      Number.isInteger(req.billingIncrement) && req.billingIncrement > 0 && /^[A-Z0-9]{3}$/.test(String(req.unitCode || ""));
    return ok ? {
      "@type": "UnitPriceSpecification",
      price: req.price,
      priceCurrency,
      billingDuration: req.billingDuration,
      billingIncrement: req.billingIncrement,
      unitCode: req.unitCode
    } : null;
  }
  if (req.type === "text") return typeof req.text === "string" && req.text.trim() ? req.text.trim() : null;
  return null;
}

function buildTier(tier, orgUrl) {
  const id = tierId(orgUrl, tier.key);
  const hasTierBenefit = [...new Set((tier.benefits || []).map((b) => TIER_BENEFIT[b]).filter(Boolean))];
  if (!id || !tier.name || !hasTierBenefit.length) return null; // name and hasTierBenefit are required

  // A points tier must state how many points are earned.
  const earnsPoints = hasTierBenefit.includes(TIER_BENEFIT.points);
  if (earnsPoints && !isPositiveNumber(tier.pointsPerCurrencyUnit)) return null;

  // An invalid requirement is not dropped silently: omitting it would claim the tier is free to join.
  const hasTierRequirement = buildTierRequirement(tier.requirement);
  if (hasTierRequirement === null) return null;

  return {
    "@type": "MemberProgramTier",
    "@id": id,
    name: tier.name,
    hasTierBenefit,
    hasTierRequirement,
    membershipPointsEarned: earnsPoints ? { "@type": "QuantitativeValue", value: tier.pointsPerCurrencyUnit } : null,
    url: isAbsoluteUrl(tier.signupUrl) ? tier.signupUrl : null
  };
}

function buildMemberProgram(program, orgUrl) {
  if (!program || !program.name || !program.description) return null; // required
  if (!/^[a-z0-9-]+$/.test(String(program.key || ""))) return null;
  const tierKeys = (program.tiers || []).map((t) => t.key);
  if (new Set(tierKeys).size !== tierKeys.length) return null; // duplicate tier @id
  const hasTiers = (program.tiers || []).map((t) => buildTier(t, orgUrl));
  if (!hasTiers.length || hasTiers.includes(null)) return null; // publish every visible tier or none
  return {
    "@type": "MemberProgram",
    "@id": `${orgUrl}#program-${program.key}`,
    name: program.name,
    description: program.description,
    url: isAbsoluteUrl(program.signupUrl) ? program.signupUrl : null, // a single URL
    hasTiers
  };
}

function buildLoyaltyProgramSchema(source) {
  // (a) Page gates, and only on the page that carries the Organization details and policies.
  if (!passesPageGates(source) || source.pageRole !== "organization_details") return null;

  const org = source.organization || {};
  if (!org.name || !isAbsoluteUrl(org.url)) return null;

  // (b) Required: name, description, at least one valid tier with name and hasTierBenefit.
  const memberProgram = (source.memberPrograms || []).map((p) => buildMemberProgram(p, org.url));
  if (!memberProgram.length || memberProgram.includes(null)) return null;

  // (c) and (d) applied in buildMemberProgram and buildTier; (e) strip empty values.
  return compact({
    "@context": "https://schema.org",
    "@type": chooseOrganizationType(org),
    "@id": `${org.url}#organization`,
    name: org.name,
    url: org.url,
    hasMemberProgram: memberProgram.length === 1 ? memberProgram[0] : memberProgram
  });
}

// Usage:
// const source = {
//   "url": "https://www.example.com/about",
//   "canonicalUrl": "https://www.example.com/about",
//   "indexable": true,
//   "contentVisible": true,
//   "pageRole": "organization_details",
//   "organization": {
//     "kind": "online_store",
//     "name": "Trailhead Outfitters",
//     "url": "https://www.example.com/"
//   },
//   "memberPrograms": [
//     {
//       "key": "basecamp",
//       "name": "Basecamp Rewards",
//       "description": "Earn points on every order and unlock member prices on gear and free 2-day shipping.",
//       "signupUrl": "https://www.example.com/rewards",
//       "tiers": [
//         {
//           "key": "trail",
//           "name": "Trail",
//           "benefits": [
//             "points"
//           ],
//           "pointsPerCurrencyUnit": 1,
//           "requirement": {
//             "type": "text",
//             "text": "Create a free online account"
//           },
//           "signupUrl": "https://www.example.com/rewards/join"
//         },
//         {
//           "key": "summit",
//           "name": "Summit",
//           "benefits": [
//             "points",
//             "member_price"
//           ],
//           "pointsPerCurrencyUnit": 3,
//           "requirement": {
//             "type": "paid_membership",
//             "price": 49,
//             "currency": "USD",
//             "billingDuration": 12,
//             "billingIncrement": 1,
//             "unitCode": "MON"
//           },
//           "signupUrl": "https://www.example.com/rewards/summit"
//         }
//       ]
//     }
//   ]
// };
// const schema = buildLoyaltyProgramSchema(source); // null when a gate or required property fails

Why Conditional Logic Is Better Than Static Templates

  • Enforces the required properties in code: no program without name, description and at least one tier, and no tier without name and a recognized hasTierBenefit value.
  • Blocks a points tier that has no membershipPointsEarned, so the points benefit is never published without its earn rate.
  • Rejects a tier whose requirement is incomplete instead of dropping hasTierRequirement, which would tell Google a paid or card tier is free to join.
  • Derives tier @id values from stable keys, so the validForMemberTier references in offers and shipping services keep resolving when tier names or prices change.
  • Reads from the same program config that renders the rewards page and the member prices, which keeps markup, visible terms and offer pricing in sync.

Property Reference

What Google's Loyalty program documentation requires and recommends, property by property. These are the same rules the SchemaCDN validator checks. Each name links to its schema.org definition.

Loyalty program properties in Google's documentation
PropertyGoogleExpectsNotes
nameRequiredTextThe name of the loyalty program.
descriptionRequiredTextThe description of the loyalty program, describing the primary benefits for members.
hasTiersRequiredMemberProgramTier, can repeatA loyalty program must have at least one tier.
hasTiers.nameRequiredTextThe name of the membership tier.
hasTiers.hasTierBenefitRequiredEnum, can repeatThe benefits of the tier: TierBenefitLoyaltyPoints and/or TierBenefitLoyaltyPrice. One of: TierBenefitLoyaltyPoints, TierBenefitLoyaltyPrice.
hasTiers.hasTierRequirementRecommendedCreditCard, MonetaryAmount, UnitPriceSpecification or TextThe requirement to join the member tier.
hasTiers.membershipPointsEarnedRecommendedQuantitativeValueThe number of loyalty points earned per currency unit spent.
hasTiers.urlRecommendedURLA URL of the page where a shopper can sign up for this tier.
urlRecommendedURLA URL of the page where a shopper can sign up for the loyalty program.

Explore Other Schema Framework Pages

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

Implement Loyalty Program Schema Frameworks Correctly

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