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:
-
612appears in Wikipedia as Somalia, but is absent from GS1's own list -
894is 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
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
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
Python, same data, same names in snake_case:
pip install gs1-prefix
from gs1_prefix import lookup
result = lookup("7310865004703")
result.range.issuing_organisation # "GS1 Sweden"
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"
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
- 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.
- Site and raw files: gs1-prefix-ranges.vercel.app
- Repo: github.com/xoocode/gs1-prefix-ranges
- npm and PyPI:
gs1-prefix
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)