Duplicate GTINs in WooCommerce

A GTIN means exactly one trade item, so two WooCommerce records sharing a code are making a claim that can't be true — and Google disapproves offers over it. CatalogLift finds every duplicate group across products and variations, including variants leaning on their parent's code.

Sales-readiness High severity Fix in CatalogLift

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.

Fair questions, straight answers.

Doesn't WooCommerce enforce unique GTINs?

Only on one path. The uniqueness check runs when a product is saved through WooCommerce's own CRUD — the editor, the official REST API. Anything that writes the meta row directly, which includes many CSV importers and plugins, walks straight past it, and WooCommerce keeps both codes without a warning.

What about variations sharing the parent's code?

That is the most common duplicate of all, and it is still wrong: each variant is a distinct trade item with its own GTIN, and Google rejects variants submitted under a shared code. CatalogLift compares products and variations together, so a parent's code copied across its variations shows up as one duplicate group.

Which record gets to keep the code?

That is a fact about your products, not something software can rule on — the code belongs to whichever item actually carries that barcode, and the others need their own real codes. There is no AI in this check. The finding shows the whole group; you set the right code on the right records, approve, and CatalogLift writes the changes to WooCommerce.

Are two products with an empty field a duplicate?

No. Records without a code are skipped here — an empty field is the missing GTIN check, a separate finding. This check only groups records whose fields hold the same non-empty code.

See it on your own catalog.

Create a free account, connect your WooCommerce store and this check runs on your real products, along with the rest.

No card required. The free plan never writes to WooCommerce.