Shopify product feed audit: find the data behind Merchant Center disapprovals
When Shopify products are disapproved or have limited visibility in Google Merchant Center, start with Merchant Center itself. Products → Needs attention lists every product-level issue and the attribute it's about. Fix that first. It's the only place that tells you Google's actual reason.
What Merchant Center doesn't show well is the pattern across your catalogue: how many variants lack a barcode, which products to fix first. That you can audit from the storefront.
The usual data problems, from Google's own requirements
- GTIN. Google wants the manufacturer's GTIN where one exists: 8, 12, 13 or 14 digits, a correct GS1 check digit, and not in the restricted (02, 04, 2) or coupon ranges. Don't invent one. Store-brand and custom products often have none. In Shopify, the GTIN is the variant's Barcode field.
- Missing GTIN or MPN. Shopify's fix: add the barcode under Inventory, or add brand and MPN in the Google & YouTube channel if there's no barcode.
- Brand. Required for most new products. No placeholders like "N/A".
- Data that doesn't match the landing page, for example price or availability. Google's data-quality policy says mismatches can lead to disapproval.
Auditing a catalogue from the storefront
Most Shopify stores serve their catalogue at /products.json (limit up to 250, plus page). It isn't a documented API, so treat it as best-effort. Two catches:
- Barcodes and image alt text aren't in
/products.json. They're in each product's own/products/{handle}.json, so sample and fetch those. - The feed stops at 25,000 products. Bigger stores need the public sitemap and collection feeds too. Compare your count with the store's.
import random, time, requests
STORE = "https://your-store.com" # your storefront domain
UA = {"User-Agent": "feed-audit-script/1.0"}
def gtin_ok(code):
d = "".join(ch for ch in str(code or "") if ch.isdigit())
if len(d) not in (8, 12, 13, 14):
return False
body, check = [int(x) for x in d[:-1]], int(d[-1])
total = sum(n * (3 if i % 2 == 0 else 1) for i, n in enumerate(reversed(body)))
return (10 - total % 10) % 10 == check
products, page = [], 1
while True:
r = requests.get(f"{STORE}/products.json", params={"limit": 250, "page": page}, headers=UA, timeout=20)
r.raise_for_status()
batch = r.json()["products"]
if not batch:
break
products += batch
page += 1
time.sleep(1)
no_type = sum(1 for p in products if not (p["product_type"] or "").strip())
no_vendor = sum(1 for p in products if not (p["vendor"] or "").strip())
no_image = sum(1 for p in products if not p["images"])
variants = bad_gtin = 0
for p in random.sample(products, min(20, len(products))): # barcodes live on each product's own JSON
full = requests.get(f"{STORE}/products/{p['handle']}.json", headers=UA, timeout=20).json()["product"]
for v in full["variants"]:
variants += 1
bad_gtin += not gtin_ok(v.get("barcode"))
time.sleep(1)
print(f"{len(products)} products | no product type: {no_type} | no vendor: {no_vendor} | no image: {no_image}")
print(f"sampled variants without a valid GTIN: {bad_gtin}/{variants}")
The check-digit test won't catch a well-formed GTIN that belongs to a different product, and it doesn't check the restricted prefixes. Treat it as a first pass.
We built this
Disclosure: Your Evenings Ltd built and sells this. Shopify Products Scraper & AI Shopping Readiness Audit on the Apify Store runs this kind of audit over any list of Shopify stores:
- It returns one row per product, with every image (with alt text) and every variant (SKU, prices, options, availability and weight). Stock counts appear only where the store publishes them. You also get one audit row per store.
- Barcode check on every variant. Turn on
readAllProductDetails, and each variant's barcode is markedvalid,missing,invalid(wrong length or check digit for GTIN-8, 12, 13 or 14) orduplicate(the same code on another variant; a 12-digit UPC and the same code as a 13-digit EAN count as one). You get a status on each variant, totals on the store's audit row, and aBARCODE-ISSUESCSV listing only the problem variants (title, handle, URL, SKU, options, barcode, status). An audit-only run costs about $0.02 per store. Reading every product takes about a second each, so catalogues above about 200 products need a longer run timeout. Any products it didn't reach are counted in the note. Try the barcode issues example. - The audit scores GTIN/barcode check digits, descriptions, product type, vendor, image alt text, SKUs, missing images and duplicate titles. It gives a 0–100 score with a per-check breakdown and example product handles to fix first.
- It reads past the 25,000-product feed limit and says whether the whole catalogue was read (
catalogueComplete). - Stores that aren't Shopify, are password-protected or block robots get a free status row with a plain note.
To be clear about what it isn't: it reads public storefront data only and changes nothing in Shopify. It can't see your Merchant Center account or Google's reason for a disapproval, and a good score doesn't guarantee approval.
FAQ
Where do I see why a Shopify product was disapproved? In Google Merchant Center, under Products → Needs attention.
Which Shopify field does Google use as the GTIN? The variant's Barcode field. For products without one, Shopify suggests adding brand and MPN in the Google & YouTube channel.
Should I make up a GTIN to clear the error? No. Google says not to guess or invent GTINs. Products without a GTIN don't need one.
Product names and logos belong to their owners. Your Evenings Ltd is not affiliated with or endorsed by them.