DEV Community

Peter
Peter

Posted on Originally published at gs1-prefix-ranges.vercel.app

The barcode prefix does not tell you where a product was made

There is a claim about barcodes that circulates every few years, usually attached to a call to boycott something: the first digits tell you which country a product was made in. Scan a jar, read the prefix, learn the origin.

It is wrong, and the organisation that issues the numbers says so directly:

Since GS1 user companies can manufacture products anywhere in the world, GS1
Prefixes do not identify the country of origin for a given product.

What the prefix actually identifies is the GS1 member organisation that allocated the number to the brand owner. A company registers with one organisation, usually the one where it has its seat, and keeps those number ranges wherever it later manufactures. A Swedish company with a 73x prefix can have every unit made in Vietnam and the barcode will not change.

I went looking for a dataset that got this right and did not find one, so I built it. It sits behind a Swedish-language lookup tool where you can paste a barcode and see the same answer without installing anything.

What annoyed me enough to build something

Most lookup services label the field country of origin. Not issuing organisation, not country of the issuing organisation. The label itself teaches the myth to everyone who uses the tool.

The npm package with the most obvious name is gs1-prefix-code-to-country-code. Last published in 2023, no TypeScript types, no keywords, and the entire API is named after the wrong idea. Install it and the mistake propagates into your codebase as a function name.

That felt like a small thing worth fixing properly.

Two sources, and what happens when they disagree

The table is reconciled against GS1's own prefix list and the English Wikipedia article List of GS1 country codes. Most entries agree. Two do not:

  • 612 appears in Wikipedia as Somalia, but is absent from GS1's own list
  • 894 is listed as Bangladesh by Wikipedia, while GS1 says the range is managed centrally

The obvious move is to pick one and move on. I kept both disagreements in the record instead, as a boolean field:

import { GS1_RANGES } from "gs1-prefix";

GS1_RANGES.filter((r) => r.sourceDisagreement);
// the two ranges where the sources conflict
Enter fullscreen mode Exit fullscreen mode

This is the part I actually care about. A dataset that silently resolves conflicts looks cleaner and tells you less. If you are building something where an unallocated prefix matters, you want to know that one of your 144 ranges rests on a single source.

Using it

npm install gs1-prefix
Enter fullscreen mode Exit fullscreen mode
import { lookup } from "gs1-prefix";

const result = lookup("7310865004703");

result.prefix;                        // "731"
result.range.issuingOrganisation;     // "GS1 Sweden"
result.range.organisationCountry;     // "Sweden"
result.range.organisationCountryCode; // "SE"
result.checkDigitValid;               // true
Enter fullscreen mode Exit fullscreen mode

Python, same data, same names in snake_case:

pip install gs1-prefix
Enter fullscreen mode Exit fullscreen mode
from gs1_prefix import lookup

result = lookup("7310865004703")
result.range.issuing_organisation   # "GS1 Sweden"
Enter fullscreen mode Exit fullscreen mode

Or skip the dependency and take the CSV, JSON or DCAT-AP file from gs1-prefix-ranges.vercel.app. CORS is open, so you can fetch it straight from a page.

The detail that trips people up

A 12-digit UPC-A is a GTIN-13 with an implied leading zero. Read the twelve digits as they are and you land on the wrong prefix, and the answer still looks plausible, which is the worst kind of bug.

lookup("012345678905").paddedFromUpc; // true
lookup("012345678905").prefix;        // "001", not "012"
Enter fullscreen mode Exit fullscreen mode

The flag is there so you can tell the two cases apart rather than wondering.

The check digit follows the same principle. It returns true, false, or null when the length carries no check digit, so "there is no check digit here" and "the check digit is wrong" are different answers.

Ranges that are not countries at all

Worth knowing before you write range.organisationCountry into a UI:

Range What it is
020-029, 200-299 Restricted distribution, defined locally
040-049 Restricted to within one company
977 Periodicals, ISSN
978-979 Books, ISBN. 979-0 is sheet music, ISMN
980 Refund receipts
981-983, 990-999 Coupons

Those carry issuingOrganisation: null and a note explaining what the range is for. Around 281 of the 1000 possible prefixes are unallocated, and the lookup returns a null range rather than guessing.

One caution if you maintain your own copy

Several GS1 member organisations publish stale versions of the list. GS1 Slovakia's still names Serbia and Montenegro, a country that stopped existing in

  1. A stale list looks exactly like a current one, so check the date on whatever you copy from.

That applies to this dataset too. It is generated from a table I maintain, and the generation date is in the JSON.

Licence and where it lives

MIT for the code, CC BY 4.0 for the data. Attribution goes to smartatest.se, where the table is maintained.

Corrections to the data are welcome as an issue. If you find a range where GS1 and Wikipedia disagree and I have not flagged it, that is the most useful thing you could tell me.

Top comments (0)