If you build product feeds for Google Shopping, bad barcodes are one of the most common reasons products get disapproved. Shopify's barcode field accepts anything, so real catalogues end up with SKUs, typos and placeholders like 0000000000000 sitting where a GTIN should be.
Before a barcode goes into a feed, it should at least pass the GS1 check digit test. It takes about 20 lines of JavaScript.
What counts as a GTIN
A GTIN is the product's global barcode number. The common formats are all GTINs:
| Format | Digits |
|---|---|
| GTIN-8 (EAN-8) | 8 |
| GTIN-12 (UPC-A) | 12 |
| GTIN-13 (EAN-13, JAN, ISBN-13) | 13 |
| GTIN-14 | 14 |
Every one is digits only, and the last digit is a check digit calculated from the others.
How the check digit works
Take every digit except the last. Starting from the right and moving left, multiply them by 3, 1, 3, 1 and so on, then add them up. The check digit is whatever brings that total up to the next multiple of 10.
For the EAN-13 4006381333931, the body is 400638133393:
4×1 + 0×3 + 0×1 + 6×3 + 3×1 + 8×3 + 1×1 + 3×3 + 3×1 + 3×3 + 9×1 + 3×3 = 89
The next multiple of 10 is 90, so the check digit is 1, which matches the last digit of the barcode.
Because the weights start from the right, the same function works for 8, 12, 13 and 14 digit codes. There's no need to pad to 14 digits first.
The code
// GS1 mod-10: weights 3,1,3,1... from the digit left of the check digit
function checkDigit(body) {
let sum = 0;
for (let i = body.length - 1, w = 3; i >= 0; i--, w = 4 - w) {
sum += Number(body[i]) * w;
}
return (10 - (sum % 10)) % 10;
}
function validateGtin(raw) {
const code = String(raw).replace(/[\s-]/g, '');
if (!/^\d+$/.test(code)) return { valid: false, reason: 'not digits only' };
if (![8, 12, 13, 14].includes(code.length)) return { valid: false, reason: `${code.length} digits` };
if (/^(\d)\1+$/.test(code)) return { valid: false, reason: 'placeholder' };
const expected = checkDigit(code.slice(0, -1));
if (Number(code.slice(-1)) !== expected) {
return { valid: false, reason: `check digit should be ${expected}` };
}
return { valid: true };
}
validateGtin('4006381333931'); // { valid: true }
validateGtin('036000291452'); // { valid: true } (UPC-A)
validateGtin('4006381333932'); // { valid: false, reason: 'check digit should be 1' }
validateGtin('0000000000000'); // { valid: false, reason: 'placeholder' }
The placeholder rule matters. A string of zeros passes the checksum, because a total of 0 gives a check digit of 0, but it isn't a real barcode.
What a valid check digit doesn't tell you
A passing checksum means the number is well formed. It doesn't mean the number is registered to your product. A few rules worth building into any feed:
- Never invent a GTIN. Google checks barcodes against the GS1 registry and disapproves fakes.
- Use the barcode for the exact variant, and don't reuse one barcode across different variants.
- Keep SKUs out of the barcode field. Internal codes aren't GTINs.
-
Leave it empty for genuine one-offs like handmade goods. Google's
identifier_existsattribute set tonotells Google not to expect one.
Doing this for a Shopify feed
This is the same check FeedShine runs on every Shopify barcode before it builds a Google Shopping feed. Only valid GTINs are sent, placeholder barcodes are skipped instead of causing disapprovals, and identifier_exists is set to no automatically when a product has no identifiers. The details are in how FeedShine handles GTINs.
If you're writing your own feed code, run every barcode through a function like validateGtin before it goes out, and drop the ones that fail rather than sending them.
Top comments (0)