Why duplicate GTINs get offers disapproved
The whole point of a GTIN is that it names one product everywhere in
the world. WooCommerce knows this — it checks uniqueness when a
product is saved through its own editor or API — but the check lives
only on that path. A CSV import or a plugin that writes the
_global_unique_id meta directly bypasses it entirely,
and the catalog ends up with two records swearing they are the same
item.
- Google disapproves the conflict. Merchant Center reads two offers with one GTIN as contradictory claims about the same product. It doesn't pick a winner — offers get disapproved until the codes tell the truth again.
- Variants under one code are the classic case. A parent's code copy-pasted across its variations, or one variant's code reused for its siblings, submits several distinct items as one. Google rejects variants sharing a GTIN for exactly this reason.
- Marketplaces and comparison engines merge or refuse. Marketplaces validate the barcode per offer against the GS1 database, and comparison engines match products to their pages by barcode — two of your records with one code either collide in the listing flow or land on the same comparison row.
- The next import spreads it further. Tools that match records by GTIN can't tell the twins apart, so an update meant for one lands on the other, and the duplicate quietly propagates into stock counts and feeds.
How to find duplicate GTINs manually
The code lives in postmeta under _global_unique_id, for
products and variations alike, with a copy in the
wc_product_meta_lookup table. This WP-CLI query lists
every code that appears on more than one published record:
wp db query "SELECT m.meta_value AS gtin,
COUNT(*) AS uses,
GROUP_CONCAT(p.ID ORDER BY p.ID) AS post_ids
FROM wp_postmeta m
JOIN wp_posts p ON p.ID = m.post_id
WHERE m.meta_key = '_global_unique_id'
AND m.meta_value <> ''
AND p.post_type IN ('product', 'product_variation')
AND p.post_status = 'publish'
GROUP BY m.meta_value
HAVING uses > 1
ORDER BY uses DESC, gtin;"
Adjust the table prefix if yours isn't wp_. A parent
and its own variations sharing a code appear here too — those rows
are duplicates, not a shortcut.
The list is the easy half. Each group still needs someone to decide which record truly carries that barcode, chase the real codes for the rest, and fix them without minting a new collision. And the answer holds for one moment — the importer that bypassed the uniqueness check last week will bypass it again next week, with nobody watching.
How CatalogLift finds it
Every scan reads the identifier field of each published product and each published variation of a published parent, and groups records by code — products and variations together, so a variant sharing its parent's code lands in the same group as any other collision. Records with an empty field are skipped, because an empty field is a different finding. Every member of a group becomes a finding at high severity, carrying the shared code, how many records claim it, and the colliding products and variations as evidence.
There is no AI on this path, because no model can know which of two records the barcode on the shelf belongs to. With the whole group in front of you, you keep the code on the record that truly owns it and give the others their real codes — from supplier data or the packaging, never invented. After you approve, CatalogLift writes the changes to WooCommerce, and the next scan verifies the group is gone. Because CatalogLift keeps watching, the import that recreates the collision surfaces as a fresh finding instead of a silent regression, and the first scan costs nothing and needs no card.