Mert Özen Further With Every Line
SEO & GEO Structured Data and Schema.org — #08

Store Information at the Organization Level: Logo, Contact Points, MerchantReturnPolicy, Shipping Policy and Precedence

Mert Özen Nov 15, 2026 11 min 7 views
Store Information at the Organization Level: Logo, Contact Points, MerchantReturnPolicy, Shipping Policy and Precedence

Return and shipping policies aren't written into every product page; they are defined once under OnlineStore at the store level, product offers point to them with @id, and only exceptions are overridden at product level. For Bookstore we produce MerchantReturnPolicy and ShippingService from a single StorePolicy object and cover Türkiye's 14-day withdrawal right and Google's precedence between feeds, Search Console and markup.

Version note: This post is based on Google Search Central's Organization, Merchant return policy, Merchant shipping policy and Merchant listing documentation as of 15 November 2026 and on schema.org 30.1; the code was built and run with .NET 10 (SDK 10.0.404), xUnit v3 4.0 and AngleSharp 1.10.

Guess First

Bookstore, the online bookstore used as the shared example across this site's series, raised its shipping fee from 49.90 TL to 59.90 TL. To put the new fee into the book pages' markup, the team adds a shippingDetails to every product's Offer; half an hour later half of the thousands of books show the old fee and half the new one, because some pages are served from the cache. Meanwhile Gül from customer service has gone into Search Console and entered a 14-day return window in the store settings. Question: which of these three sources does Google take shipping and return information from, and in how many places should the fee change have been written?

In the previous two posts we marked up the book page's price, stock and rating. In this post we move on to the store itself: logo, contact details, return and shipping policy. We define all of it once, at the store level.

The Store Side of Organization Markup

In the third post of the series the Organization node was there for the site name and the logo; Google's Organization documentation still defines no required properties and recommends adding everything that applies. For merchants the job gets bigger: the documentation says details in the merchant knowledge panel such as return policy, address and contact information can come from this markup. The recommended properties include address, contactPoint, email, telephone, logo, legalName, sameAs and identifier fields: vatID, taxID, iso6523Code, duns, leiCode, naics, globalLocationNumber. For DUNS and LEI, Google now encourages iso6523Code (with the 0060: and 0199: prefixes) instead of the separate fields.

Two technical recommendations concern Bookstore directly. The first is the type: for an ecommerce site the most specific subtype of Organization, OnlineStore, is recommended (instead of OnlineBusiness); with a physical store, the matching subtype of LocalBusiness. The second is placement: this information belongs on the home page or on a single page that describes the organization, not on every page. For the logo Google asks for at least 112x112 pixels, a crawlable URL and an image that looks right on a white background.

Bookstore's sample data has no tax number or full address; a real store should add taxID (the tax identification number in Türkiye) and legalName. Google wants taxID to match the country given in address.

This section is based on the official documentation.

One Definition, the Same @id Everywhere

In Bookstore the store node lives in two forms. Every page carries a short OnlineStore node with the same @id: name, URL, logo, sameAs. The full definition is only on the "Shipping and returns" page, because Google recommends defining return and shipping policies on a single page that describes them, and the markup has to match the visible content. The return part of the node on that page:

{
  "@type": "OnlineStore",
  "@id": "https://bookstore.example/#organization",
  "name": "Bookstore",
  "url": "https://bookstore.example/",
  "logo": "https://bookstore.example/images/logo-512.png",
  "sameAs": [
    "https://github.com/bookstore-example"
  ],
  "contactPoint": {
    "@type": "ContactPoint",
    "contactType": "customer service",
    "email": "support@example.com"
  },
  "address": {
    "@type": "PostalAddress",
    "addressLocality": "Istanbul",
    "addressCountry": "TR"
  },
  "hasMerchantReturnPolicy": [
    {
      "@type": "MerchantReturnPolicy",
      "@id": "https://bookstore.example/shipping-and-returns#return-policy",
      "applicableCountry": [
        "TR"
      ],
      "returnPolicyCountry": "TR",
      "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
      "merchantReturnDays": 14,
      "returnMethod": "https://schema.org/ReturnByMail",
      "returnFees": "https://schema.org/FreeReturn",
      "refundType": "https://schema.org/FullRefund"
    }
  ]
}

SiteGraph produces both forms from the same identity; the full node is the short node extended with with:

public string ReturnPolicyId => $"{baseUrl}/shipping-and-returns#return-policy";
public string ShippingServiceId => $"{baseUrl}/shipping-and-returns#standard-shipping";

// Every page gets the same short store node; details live on the policy page only
public OnlineStoreNode Organization() =>
    new(OrganizationId, "Bookstore", $"{baseUrl}/", $"{baseUrl}/images/logo-512.png",
        ["https://github.com/bookstore-example"]);

The policy values live in a single record. Both the sentences on the policy page and the JSON-LD are produced from this object; changing the fee is a one-line job:

using System.Globalization;

namespace Bookstore.Web.Store;

// The store's standard terms; the policy page and the JSON-LD are both built from this object
public sealed record StorePolicy(
    string Country,
    int ReturnDays,
    decimal ShippingFee,
    decimal FreeShippingFrom,
    int HandlingDaysMax,
    int TransitDaysMin,
    int TransitDaysMax,
    TimeOnly CutoffTime,
    TimeSpan UtcOffset,
    string Currency = "TRY")
{
    // Sample policy: 14 days to return after delivery, free shipping from 500 TL
    public static StorePolicy Bookstore { get; } = new(
        Country: "TR",
        ReturnDays: 14,
        ShippingFee: 49.90m,
        FreeShippingFrom: 500.00m,
        HandlingDaysMax: 1,
        TransitDaysMin: 1,
        TransitDaysMax: 3,
        CutoffTime: new TimeOnly(14, 0),
        UtcOffset: TimeSpan.FromHours(3));

    // ISO 8601 time with offset, e.g. 14:00:00+03:00
    public string CutoffTimeWithOffset =>
        new DateTimeOffset(DateOnly.MinValue.ToDateTime(CutoffTime), UtcOffset)
            .ToString("HH:mm:sszzz", CultureInfo.InvariantCulture);
}

MerchantReturnPolicy and the 14-Day Right of Withdrawal

Google's return policy documentation offers two options for the store-level MerchantReturnPolicy: either you describe the policy with applicableCountry and returnPolicyCategory, or you only point to the policy page with merchantReturnLink. Bookstore uses the first. With MerchantReturnFiniteReturnWindow, merchantReturnDays is required, and the number is counted from the delivery date. There are three values for return fees: free (FreeReturn), shipped back by the customer (ReturnFeesCustomerResponsibility) and charged by the merchant (ReturnShippingFees, with the amount in returnShippingFeesAmount).

In Türkiye this number has a legal counterpart. Article 9 of the Distance Contracts Regulation, published in the Official Gazette on 27 November 2014, gives consumers the right to withdraw within fourteen days without giving a reason, and for contracts on delivery of goods the period starts on the day the goods are received. That matches Google's "number of days from the delivery date" definition. Article 15 lists the exceptions; among them are printed books whose protective packaging has been opened and intangible goods delivered to the consumer instantly. This post is not a legal assessment; the point is the limit of the markup: MerchantReturnPolicy can't carry conditions such as "if the packaging hasn't been opened". The store-level markup states the general rule and the conditions stay in the text of the policy page. If a product really has a different rule (an e-book downloaded instantly, for example), Google's way is to write that product's own MerchantReturnPolicy on its Offer.

ShippingService: Writing the Shipping Policy Once

Store-level shipping markup is new: on 12 November 2025 Google published the Merchant shipping policy documentation and started supporting ShippingService defined under Organization with hasShippingService. The only required property is shippingConditions; each condition describes a fee and a delivery time for a destination, an order value range, a weight or a number of items. Bookstore's code:

public OnlineStoreNode Store(StorePolicy policy)
{
    string[] weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"];
    var transit = new ServicePeriodNode(new DaysNode(policy.TransitDaysMin, policy.TransitDaysMax), weekdays);
    var region = new DefinedRegionNode(policy.Country);

    var returns = new MerchantReturnPolicyNode(
        Id: ReturnPolicyId,
        ApplicableCountry: [policy.Country],
        ReturnPolicyCountry: policy.Country,
        ReturnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow",
        MerchantReturnDays: policy.ReturnDays,
        ReturnMethod: "https://schema.org/ReturnByMail",
        ReturnFees: "https://schema.org/FreeReturn",
        RefundType: "https://schema.org/FullRefund");

    var shipping = new ShippingServiceNode(
        Id: ShippingServiceId,
        Name: "Standard shipping",
        FulfillmentType: "https://schema.org/FulfillmentTypeDelivery",
        HandlingTime: new ServicePeriodNode(new DaysNode(0, policy.HandlingDaysMax), weekdays,
            CutoffTime: policy.CutoffTimeWithOffset),
        ShippingConditions:
        [
            // below the threshold: fixed fee; maxValue is just under the free-shipping amount
            new(region, new MonetaryAmountNode(policy.Currency, MaxValue: policy.FreeShippingFrom - 0.01m),
                new MonetaryAmountNode(policy.Currency, Value: policy.ShippingFee), transit),
            new(region, new MonetaryAmountNode(policy.Currency, MinValue: policy.FreeShippingFrom),
                new MonetaryAmountNode(policy.Currency, Value: 0m), transit),
        ]);

    return Organization() with
    {
        ContactPoint = new ContactPointNode("customer service", "support@example.com"),
        Address = new PostalAddressNode("Istanbul", policy.Country),
        HasMerchantReturnPolicy = [returns],
        HasShippingService = [shipping],
    };
}

The first condition of the resulting ShippingService (orders under 500 TL) looks like this; the second condition has the same structure with minValue: 500.00 and value: 0:

{
  "@type": "ShippingService",
  "@id": "https://bookstore.example/shipping-and-returns#standard-shipping",
  "name": "Standard shipping",
  "fulfillmentType": "https://schema.org/FulfillmentTypeDelivery",
  "handlingTime": {
    "@type": "ServicePeriod",
    "duration": {
      "@type": "QuantitativeValue",
      "minValue": 0,
      "maxValue": 1,
      "unitCode": "DAY"
    },
    "businessDays": [
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday"
    ],
    "cutoffTime": "14:00:00+03:00"
  },
  "shippingConditions": [
    {
      "@type": "ShippingConditions",
      "shippingDestination": {
        "@type": "DefinedRegion",
        "addressCountry": "TR"
      },
      "orderValue": {
        "@type": "MonetaryAmount",
        "currency": "TRY",
        "maxValue": 499.99
      },
      "shippingRate": {
        "@type": "MonetaryAmount",
        "currency": "TRY",
        "value": 49.90
      },
      "transitTime": {
        "@type": "ServicePeriod",
        "duration": {
          "@type": "QuantitativeValue",
          "minValue": 1,
          "maxValue": 3,
          "unitCode": "DAY"
        },
        "businessDays": [
          "Monday",
          "Tuesday",
          "Wednesday",
          "Thursday",
          "Friday"
        ]
      }
    }
  ]
}

Three details. If more than one condition applies to a product, Google uses the lowest shipping cost, and with equal costs the fastest delivery; the second condition overriding the first for orders of 500 TL or more follows from that rule too, but writing the ranges explicitly is clearer for the developer reading the code. cutoffTime is written with a time zone (14:00:00+03:00); for orders received after that time Google adds one day to the delivery estimate. In DefinedRegion, a province-level region (addressRegion) is supported by Google only for the US, Australia and Japan, and postal codes only for Australia, Canada and the US; for Türkiye the region stays at country level.

From the Offer to the Policy with @id

Policies defined at the store level apply to all products by default. Google also documents using @id to link unambiguously from an Offer to a global policy on another page: hasMerchantReturnPolicy directly for returns, and hasShippingService inside shippingDetails for shipping. Bookstore does this:

// the store-wide policies apply; the offer only names them
HasMerchantReturnPolicy: new IdRef(ReturnPolicyId),
ShippingDetails: new OfferShippingDetailsNode(new IdRef(ShippingServiceId)));
{
  "hasMerchantReturnPolicy": {
    "@id": "https://bookstore.example/shipping-and-returns#return-policy"
  },
  "shippingDetails": {
    "@type": "OfferShippingDetails",
    "hasShippingService": {
      "@id": "https://bookstore.example/shipping-and-returns#standard-shipping"
    }
  }
}

Here is the answer to the opening scenario. The shipping fee is not written into thousands of Offers one by one; the value is in one place, StorePolicy. Writing a product's own shippingDetails exists only for exceptions, and Google reminds us that product-level policies support a subset of the store-level properties.

Precedence: Which Source Does Google Believe?

Shipping and return information can reach Google in several ways, and the merchant listing documentation gives an order of precedence:

08-policy-precedence-en.png

From strongest to weakest: product-level feeds submitted in Merchant Center, settings in the Content API for Shopping, settings in Merchant Center or Search Console, product-level merchant listing markup, and last the Organization-level markup. The list on the separate return and shipping policy pages lacks the first item and has four levels; Google's example there is clear: if you have return markup on your site and enter return settings in Search Console, Google uses only the information from Search Console. If there are two different return markups at product and store level, the product-level one applies.

That is why the second scenario in the opening is dangerous: if the setting Gül entered says the same as the markup, there's no problem, but if someone later updates only the markup, Google keeps using the old setting. The rule is simple: decide which source you use and write a policy change there first. Bookstore doesn't use Merchant Center; the single source is the markup, and the markup's single source is StorePolicy.

This section is based on the official documentation.

Tests, and an Old Bug the Tests Found

The new test class checks three things: does the markup on the policy page match the visible text (14 days, 49.90 TL), is the store node short on every page other than the policy page, and do the @id references on the book page point to the policies defined on the policy page. The last one is a cross-page check, because the reference goes to a node on another page:

{
    HttpClient client = factory.CreateClient();
    JsonLdPage policyPage = await JsonLdPage.LoadAsync(client, "/shipping-and-returns");
    JsonLdPage bookPage = await JsonLdPage.LoadAsync(client, "/books/dune-paperback");

    JsonElement store = StoreNode(policyPage);
    JsonElement offer = bookPage.Single("Product").GetProperty("offers");

    // the @id references cross pages, so the test resolves them against the policy page
    Assert.Equal(
        store.GetProperty("hasMerchantReturnPolicy")[0].GetProperty("@id").GetString(),
        offer.GetProperty("hasMerchantReturnPolicy").GetProperty("@id").GetString());
    Assert.Equal(
        store.GetProperty("hasShippingService")[0].GetProperty("@id").GetString(),
        offer.GetProperty("shippingDetails").GetProperty("hasShippingService").GetProperty("@id").GetString());
}

The first run failed one test:

failed Bookstore.Web.Tests.StorePolicyTests.Other_pages_carry_only_the_short_store_node(path: "/authors/ada-kaplan") (334ms)
  Assert.Single() Failure: The collection did not contain any matching items
Test run summary: Failed!
  total: 28
  failed: 1
  succeeded: 23
  skipped: 4

Since the third post, the author page had referenced the store's @id through Person.worksFor and the site node through ProfilePage.isPartOf, but defined neither on the same page. The dangling @id test from the fifth post only looked at the post's author references; testing the "short store node on every page" rule exposed the gap. The fix was to add Organization() and WebSite() to the graph returned by Profile():

$ dotnet test --project Bookstore.Web.Tests
Test run summary: Passed!
  total: 28
  failed: 0
  succeeded: 24
  skipped: 4

Production Notes

Make policy changes in one place and account for cache lifetimes. The short store node is on every page, but the policy is on one page only; book pages staying in the cache no longer publish a stale fee, because they carry the @id, not the fee.

Talk to the team before entering store settings in Search Console or Merchant Center. These settings take precedence over the markup, and Google itself writes that account-level shipping and return settings in Search Console are automatically added to Merchant Center.

If you have a loyalty program (MemberProgram, validForMemberTier), it is defined in the same place, but if you define member shipping benefits Google requires at least one regular, non-member shipping service as well. Bookstore has no such program for now.

Common Pitfalls

Repeating the same policy on every product page with different values. A site that says 14 days in one place and 15 in another doesn't tell Google which one is right; since product-level markup ranks higher, the wrong one may win.

Search Console store settings contradicting the markup. Search Console always wins; updating the markup alone is not enough.

Turning conditional exceptions into the general rule. Writing MerchantReturnNotPermitted for the whole store because "books with opened packaging can't be returned" gives wrong information for every order that can be returned.

Defining province-level shipping regions. addressRegion isn't supported for Türkiye; if Istanbul and the rest of the country have different fees, you can't express that with a region. Giving the upper bound with maxValue, which Google supports for shippingRate, is the right move in that case.

Adding a store rating or reviews to OnlineStore. The self-serving rule from the previous post applies to this node too.

A Small Experiment

★☆☆ Change StorePolicy.ShippingFee to 59.90, run the tests and count how many files you touched.

★★☆ Add a separate fee for orders over 2.5 kilograms: add a weight field (QuantitativeValue, unitCode KGM) to ShippingConditionsNode and write a sentence on the policy page that describes it. How should the test verify the new sentence?

★★★ Add a product type for instantly downloaded e-books to Bookstore and write a policy with MerchantReturnNotPermitted only on their Offer. The store-level policy must stay the same for the other books; show it with a test.

Bookstore's book page, its reviews and its store information now have the structure Google's shopping experiences expect. In the last post of the series we look at the rest of the gallery: short recipe cards for Event, Discussion forum, Q&A, Image metadata and others, and which of them don't fit Bookstore.