GS1 2D Barcodes at Retail POS: A Developer's 2027 Readiness Checklist
By the end of 2027, GS1’s industry goal is for retail POS systems to read both today’s 1D barcodes and GS1-compliant 2D symbols carrying a GTIN, lot or expiry date — and resolve either carrier to one product identity. It is a capability goal, not a legal mandate, and EAN/UPC does not disappear.
The failure this exposes is easiest to see at the till. A shampoo bottle carries a GS1 DataMatrix with batch and expiry, plus the EAN-13 printed on its old label artwork. The scanner reads both, and the checkout rings up two bottles. This is a data-model failure: the POS has treated “symbols decoded” as “items purchased”.
For an integrator, readiness comes down to five decisions: which symbologies to accept, how to split the payload by the application-identifier table, which GTIN representation to emit, how to commit one cart line per package presentation, and which legacy behaviour must not regress.

Key Takeaways
- The symbology is not the payload format. A Data Matrix and a GS1 DataMatrix are the same symbology with different content; only the presence of FNC1 in the first position (and the AIs that follow) makes it GS1.
- Split by the AI table, not by punctuation. Fixed-length data is consumed by length, variable-length data runs to the next FNC1 byte or the end of the string. The table is prefix-free, which is what makes the split unambiguous.
- A decoder that drops FNC1 turns a batch number into a plausible but wrong value. AI
10allows 20 characters; a 21-character reading is provably wrong, and the right response is to reject the scan rather than to guess. - Emit GTIN-14, always. AI
01already gives 14 digits; a legacy EAN-13 is the same product with a leading zero. Never renumber a GTIN when you reprint a pack. - De-duplicate a presentation, not a basket. Two symbols on one pack are one line; two identical bottles scanned one after the other are two lines.
- A GS1 Digital Link is not a replacement for the element string. It is the same data written as a URL, and a POS must be able to accept it without a network round trip.
Try the checkout rule in a browser

In the dual-marked level, the checkout groups both carrier reads before committing one $2.50 cart line.
The companion example is a single-canvas checkout game. It renders the symbols, groups the carrier reads by package presentation and shows the POS rule behind every cart change:
The five scenarios run in the browser; the simulated part needs no camera or SDK license.
Scan one pack with both EAN-13 and QR, scan two identical packs in separate presentations, then create a two-GTIN conflict in the sandbox. The cart decision and the rule that produced it appear beside the package. The simulation is kept separate from the SDK image decode, so an SDK failure cannot become a fabricated success.
The lab uses EAN-13 and a GS1 Digital Link QR. It is the visual counterpart to Decision 4 below, not a substitute for the DataMatrix parser cases in the acceptance matrix.
What GS1 actually committed to
The 2027 date is an industry POS capability goal, not a mandate covering every barcode. GS1’s retail programme — for years called Sunrise 2027, now usually “Ambition 2027” — asks that a checkout be able to read a 1D symbol and a GS1-compliant 2D symbol, and get the same product identity from either. GS1’s own background note states what the date is for and what it is not (GS1: why 2027?).
Three consequences follow, and each one removes work rather than adding it:
- This is not a mandate. No regulator is going to fine a retailer for scanning an EAN-13 in 2028. The pressure is commercial: brand owners want lot and expiry data at the till, and the POS that cannot read it is the one that blocks the rollout.
- 1D stays. GS1 DataBar for variable-measure items and EAN/UPC for everything already printed are not going away. A “2D migration” that stops reading 1D symbols is a regression, not a migration.
- One pack can carry both, and the POS must commit once. The GS1 implementation guideline for 2D at retail POS (release 1.1.0) addresses this directly: when several on-pack symbols identify the same trade item, the system processes one GTIN and issues one POS acknowledgement — one cart line or one beep, not one per symbol. That is the de-duplication requirement many integrations miss.
Decision 1 — Which symbologies must a POS accept?
Most of the formats are already in a retail scanner’s format list; the trap is assuming that enabling a symbology is the same as accepting GS1 data.
| Format | What it carries | Why the POS needs it |
|---|---|---|
| EAN-13, UPC-A, UPC-E | A bare GTIN, no AIs | The installed base. Every existing pack. |
| GS1 DataBar (Omnidirectional, Expanded, Limited, Stacked) | A bare GTIN usually; Expanded can carry weight and price | Variable-measure produce and meat; small packs where an EAN-13 will not fit |
| ITF-14 | A bare GTIN-14 | Corrugated cartons, case-level scanning |
| GS1-128 | AI element string | Logistics and case labels |
| GS1 DataMatrix | AI element string | The main 2D carrier at item level: GTIN + expiry + batch + serial |
| GS1 QR Code | AI element string, often as a GS1 Digital Link URI | Consumer-facing packs; the same symbol serves the customer’s phone and the till |
A distinction that frequently causes confusion is between the plain-symbology rows and the two GS1 2D rows. A plain Data Matrix and a GS1 DataMatrix are the same symbology; the difference is encoding. A GS1 symbol starts with FNC1 in the first position, which is what tells a conformant decoder “this is an element string, not arbitrary data”. Decoders that expose the AIM symbology identifier report it as ]d2 for GS1 DataMatrix, ]Q3 for GS1 QR Code, ]C1 for GS1-128 and ]e0 for GS1 DataBar; the plain forms report ]d1, ]Q1 and so on.
The check in the pipeline is therefore not “did I get a DataMatrix?” but:
if (symbology is DataMatrix or QR) and (payload is not a GS1 element string):
route to the non-GS1 path, do not attempt an AI split
A GS1 QR Code that carries a Digital Link URI (https://id.gs1.org/01/…) is a special case worth handling explicitly — see Parsing a GS1 Digital Link without breaking checkout.
Decision 2 — How do you split the payload?
A GS1 element string is a concatenation: 01 + 14 digits, then 17 + 6 digits, then 10 + up to 20 characters, and so on. There is no delimiter between fields. The only things that make the split possible are the AI table and the FNC1 byte.
- Fixed-length AIs are consumed by length.
01always takes 14 characters,17always takes 6. No separator follows them. - Variable-length AIs run to the next FNC1 (
0x1D) or to the end of the payload. AI10is the usual suspect. - The table is prefix-free. No two-digit AI is the prefix of a three- or four-digit one, so “longest match wins” is safe. That property is why a length-driven split works at all.
Here is what the GS1 barcode scanner demo returns for a Sunrise 2027 pack — a GS1 DataMatrix printed beside a legacy EAN-13, decoded and parsed by the page’s own AI table:
{
"elementString": "01009506001375621728081310LOT-32763|21SN19531256",
"count": 4,
"hri": "(01)00950600137562(17)280813(10)LOT-32763(21)SN19531256",
"digitalLink": "https://id.gs1.org/01/00950600137562/10/LOT-32763/21/SN19531256?17=280813",
"gtin": { "ai": "01", "value": "00950600137562", "verified": true }
}
The | is the FNC1 separator, normalised. Now remove it — which is what a decoder that returns a flat string does — and the same payload parses into something that looks entirely reasonable and is wrong:
{
"elements": [
{ "ai": "01", "value": "00950600137562", "verified": true },
{ "ai": "17", "value": "280813" },
{ "ai": "10", "value": "LOT-3276321SN1", "terminatedBy": "end" },
{ "ai": "95", "value": "31256" }
],
"warnings": [
"The decoder returned no FNC1 separators, so the length of AI 10 was taken from the AI table rather than from a separator.",
"AI 10 read 21 characters, more than the 20 this AI allows, so the value was re-split."
]
}
LOT-3276321SN1 is not a batch number. It is the batch number with the serial number glued onto it, and the only reason anyone can prove that is the length rule: AI 10 allows at most 20 characters, and this reading used 21.
Two behaviours are defensible here, and they serve different purposes. The demo re-splits at the longest head whose remainder still parses, and reports that it did — which is right for a diagnostic tool, because you want to see the plausible-but-wrong answer next to the rule that condemns it. A POS integration should fail closed: no GTIN, no line, ask the cashier to rescan. A wrong batch number in a recall system is worse than a scan that needs repeating.
Decision 3 — Which GTIN do you emit?
One value, one representation: GTIN-14 in the product table. AI 01 already carries 14 digits, so the 2D path needs no conversion. Common conversion bugs occur on the legacy path:
- An EAN-13 is a GTIN-14 whose packaging indicator is
0.950600137562and00950600137562are the same product. Zero-pad on the way in, and never strip the zero on the way out. - UPC-E must be expanded to UPC-A before zero-padding; the compressed form is an encoding, not an identifier.
- A GTIN-14 with a non-zero packaging indicator (
1…8) is a case or a multipack, not the unit. Do not collapse it into the unit GTIN. - Never renumber a GTIN because you moved to 2D. The GTIN is printed on packaging that was manufactured months ago and is sitting in a warehouse. GS1’s guidance is to keep the existing GTIN; a new symbol with a new number is a different product to every system downstream.
Decision 4 — How do you avoid double counting?
The rule that satisfies the GS1 guideline is small, but its scope matters: an implementation that keys on GTIN alone breaks a routine purchase — buying two of the same item.
De-duplicate reads that belong to one presentation of goods. Never de-duplicate across a basket or a transaction.
A customer lifts a bottle, the scanner reads the DataMatrix and the EAN-13 in the same presentation, and that is one line. The cashier scans a second identical bottle, which is a separate presentation, and that is a second line. If you key on GTIN alone for the whole transaction, the second bottle vanishes from the receipt; if you key on the decoded symbol, the first bottle appears twice.
// reads: everything decoded during one presentation (one frame burst, one
// trigger pull, one item placed on the scale)
function dedupePresentation(reads) {
const lines = new Map();
for (const read of reads) {
if (!read.gtin) continue; // not a product; leave it alone
const key = read.gtin.padStart(14, '0'); // GTIN-13 and GTIN-14 collapse here
if (lines.has(key)) lines.get(key).sources.push(read.symbology);
else lines.set(key, { gtin: key, sources: [read.symbology] });
}
return [...lines.values()];
}
Run against the two reads from the pack above, that returns a single line with two sources:
[{ "gtin": "00950600137562", "sources": ["GS1 DataMatrix", "EAN-13"] }]
The sources array is worth keeping. When a store reports “the scanner double-scanned”, the array tells you whether the POS was handed two symbols or one symbol twice — two different bugs with two different fixes.
Decision 5 — What legacy behaviour must not regress?
A 2D rollout is usually a firmware change, a middleware upgrade and a POS release delivered together, which is when quiet regressions tend to appear. A practical regression checklist:
| Behaviour | Why it breaks, and what to test |
|---|---|
| EAN-13 / UPC-A read at full trigger speed | A new format list that enables 2D at the cost of 1D scan rate is a net loss. Time 100 scans before and after. |
| Check digit validated on every path | Some 2D middleware forgets it because the AI parser “already checked the GTIN”. It did not check the SSCC or the GLN. |
| UPC-E expanded correctly | A new parser that assumes 13 digits turns a valid UPC-E into a wrong GTIN-14 silently. |
Variable-measure price embedded (02/37 on DataBar Expanded) |
The weight and price AIs must still drive the line total, not just the GTIN. |
| Keyboard-wedge output unchanged | Handheld scanners in HID mode emit a keystroke stream; a middleware that now expects JSON breaks every legacy lane. |
| Manual keypad / PLU path untouched | The fallback when a label is damaged. Nobody tests it until the first damaged label. |
Parsing a GS1 Digital Link without breaking checkout
A GS1 Digital Link is the same element data written as a URL: https://id.gs1.org/01/<gtin>/10/<lot>/21/<serial>. The primary key is always /01/<gtin>; other AIs appear either as path segments or as query parameters (?17=280813), and both forms exist on real packaging — the demo above emits the path form for AI 10 and 21 and the query form for 17.
Three rules keep it from becoming a checkout outage:
- Accept it offline. The identifier is in the symbol; resolution is a separate, later step. A POS that fetches
id.gs1.orgbefore ringing up an item has put a network dependency in the payment path. - Do not confuse it with the element string.
https://…/01/00950600137562is not an AI element string and will not parse as one. Detect the URI form first, then read AIs out of it. - Resolution is not authentication. A resolvable link proves the link resolves. Signature verification and brand-owner attestation are separate mechanisms, and this article does not claim either.
const knownAI = (ai) => Boolean(FIXED[ai] || VARIABLE[ai]);
function parseDigitalLink(url) {
const u = new URL(url);
const segments = u.pathname.split('/').filter(Boolean).map(decodeURIComponent);
const elements = [];
for (let i = 0; i + 1 < segments.length; i += 2) {
const ai = segments[i];
if (knownAI(ai)) elements.push({ ai, value: segments[i + 1], source: 'path' });
}
for (const [ai, value] of u.searchParams) {
if (knownAI(ai)) elements.push({ ai, value, source: 'query' });
}
return { gtin: elements.find((e) => e.ai === '01')?.value ?? null, elements };
}
An acceptance matrix you can run in an afternoon
The matrix below is the one this article was tested against. Every row is a payload, not a photograph, so it can be asserted exactly; the images that carry those payloads can be regenerated on demand.
Download the acceptance matrix as CSV — 12 cases, expected GTIN, expected elements and the pass rule for each. <GS> in the payload column is the FNC1 byte, 0x1D.
| # | Case | Expected | What a PASS proves |
|---|---|---|---|
| A1 | GS1 DataMatrix, GTIN only | PASS, GTIN 00950600137562, 1 element |
The 2D path produces an identity at all |
| A2 | GTIN + expiry + batch + serial | PASS, 4 elements | FNC1 handling on a variable-length AI |
| A3 | A2 with FNC1 removed | REJECT | The parser fails closed instead of inventing a lot number |
| A4 | DataBar, bare GTIN, no AI | PASS + warning | AI 01 inference is disclosed, not silent |
| A5 | Legacy EAN-13 | PASS, same GTIN-14 | Zero-padding on the legacy path |
| A6 | A2 and A5 on one pack | PASS, one line | The GS1 guideline’s one-acknowledgement rule |
| A7 | GTIN + weight + price | PASS, 0.125 kg, 15.00 |
Variable-measure AIs still drive the total |
| A8 | GTIN with a bad check digit | FAIL | Nobody rounds a check digit to make a sale |
| A9 | Plain Data Matrix, not GS1 | REJECT | A 2D code is not automatically a product |
| A10 | Variable AI last, no trailing FNC1 | PASS | End of payload terminates a variable AI |
| A11 | Expiry with day 00 |
PASS, 2028-08-31 |
The day-00 rule is applied, not passed through |
| A12 | Digital Link URI in a GS1 QR Code | PASS, offline | URI form parsed without a network call |
To build the images: open the GS1 barcode generator, choose the Retail 2D (Sunrise 2027) preset — it prints a GS1 DataMatrix and derives the EAN-13 from the same GTIN, which is exactly case A6 — and download the label PNG. For custom cases, type the AIs yourself. Then scan the PNG in the GS1 barcode scanner and compare against the matrix row.
For volume rather than one pack at a time, the barcode scanner demo reads a whole dataset ZIP with ground truth and scores recall, precision, symbology agreement and box overlap — the same method behind the barcode benchmark index.
A row that FAILs is information, not a verdict on a vendor. Cameras, print density and ambient light decide whether a symbol is readable at all; the matrix decides whether the software interprets what it read.
Parsing reference implementation
The JavaScript below was run against every case in the matrix before this article was published. It needs no SDK: it starts after the decoder has handed you a payload string, the layer integrations commonly misunderstand.
JavaScript — parse, then de-duplicate:
const GS = String.fromCharCode(29);
const FIXED = {
'00': 18, '01': 14, '02': 14, '11': 6, '13': 6, '15': 6, '17': 6, '20': 2,
'3103': 6, '3922': 6
};
const VARIABLE = { '10': 20, '21': 20, '22': 20, '30': 8, '240': 30, '400': 30, '410': 13 };
// Three spellings of FNC1 in the wild: the byte, "{GS}", and the AIM "]C1".
const normalize = (raw) => String(raw ?? '')
.replace(/\{GS\}/g, '|').replace(/\{FNC1\}/g, '|').replace(/\]C1/g, '|')
.replace(new RegExp(GS, 'g'), '|').replace(/[\r\n]/g, '')
.replace(/^\|+/, '').replace(/\|+$/, '');
// Mod-10 over the digits before the check digit, weights 3,1 from the right.
function checkDigit(value) {
let sum = 0;
for (let i = 0; i < value.length - 1; i++) {
sum += Number(value[value.length - 2 - i]) * (i % 2 === 0 ? 3 : 1);
}
return (10 - (sum % 10)) % 10 === Number(value[value.length - 1]);
}
function parseElementString(raw) {
const text = normalize(raw);
const out = { elements: [], errors: [], warnings: [], gtin: null, checkDigitValid: null };
// A bare 12-14 digit payload is a GTIN the symbology announced. Decide this
// before the AI table: "00950600137562" begins with AI 00 (SSCC, 18 chars).
if (/^\d{12,14}$/.test(text)) {
out.warnings.push('bare GTIN: AI 01 inferred from the symbology');
out.elements.push({ ai: '01', value: text.padStart(14, '0') });
out.gtin = text.padStart(14, '0');
out.checkDigitValid = checkDigit(out.gtin);
return out;
}
let pos = 0;
while (pos < text.length) {
const ai = [4, 3, 2].map((n) => text.slice(pos, pos + n))
.find((candidate) => FIXED[candidate] || VARIABLE[candidate]);
if (!ai) { out.errors.push('no AI at offset ' + pos); return out; }
pos += ai.length;
let value;
if (FIXED[ai]) {
value = text.slice(pos, pos + FIXED[ai]);
pos += FIXED[ai];
} else {
const end = text.indexOf('|', pos);
value = text.slice(pos, end === -1 ? text.length : end);
pos = end === -1 ? text.length : end + 1;
if (value.length > VARIABLE[ai]) {
out.errors.push('AI ' + ai + ' overruns its maximum length');
return out;
}
}
out.elements.push({ ai, value });
}
const gtin = out.elements.find((e) => ['01', '02', '03', '04'].includes(e.ai));
out.gtin = gtin ? gtin.value : null;
out.checkDigitValid = gtin ? checkDigit(gtin.value) : null;
return out;
}
The JavaScript parser returns no GTIN at all when a payload is rejected, which is the point: the failure state has to be “no product”, not “probably this product”.
What not to do
- Do not strip leading zeros to “normalise” a GTIN-14 into a GTIN-13. You have just made the 2D path and the legacy path disagree about the same bottle.
- Do not assume a 2D code is GS1. A QR code on a coupon, a Data Matrix on a tyre wall and a GS1 DataMatrix on a medicine box arrive as the same symbology to the decoder.
- Do not trust a GTIN without the check digit. It is four lines of arithmetic and catches a common silent failure in the chain: a decoder that dropped a character.
- Do not de-duplicate across the basket. Two identical items are not a duplicate.
- Do not require network resolution at the till. See above.
- Do not claim ISO verification because you ran a scanability check. A software preflight measures pixels; ISO/IEC verification is a calibrated process with a defined aperture, and the two are not interchangeable.
Common Developer Questions
Do I have to support both GS1 DataMatrix and GS1 QR Code?
For item-level retail, GS1 DataMatrix is the workhorse and GS1 QR Code is often chosen when the same symbol has to serve a customer’s phone. A POS that accepts only one will encounter packs it cannot read. If you must sequence the work, do DataMatrix first and QR second — the parser is the same; the only new code is the Digital Link URI branch.
What does “GS1-compliant 2D” mean, precisely?
It means a 2D symbol whose payload is a GS1 element string — FNC1 in the first position, then AIs and their values per the General Specifications — or a GS1 Digital Link URI. It does not mean “any 2D symbol that happens to contain a GTIN-looking number”.
My scanner returns the payload with (01) and brackets around every AI. What is that?
That is the human readable interpretation, not the encoded data. (01)00950600137562 is what GS1 prints under the symbol; the encoded payload is 0100950600137562. HRI is for humans and for logs — feeding it back into a parser that expects the raw form produces an immediate “no AI at offset 0” error, which is at least a loud failure.
Where do lot and expiry come from if the symbol only carries a GTIN?
They do not exist in that payload. GS1 DataBar and EAN-13 carry identity only; a GS1 DataMatrix or GS1 QR Code can also carry batch and expiry. A POS that stores those fields has to record which carrier supplied them; otherwise a later 1D scan can overwrite values supplied by an earlier 2D read.
Is the 2027 date going to slip?
That is a business question, not a technical one, and this article does not forecast it. The engineering argument for starting now does not depend on the date: a POS that de-duplicates a dual-symbol pack and validates a check digit correctly is a better POS in 2026 as well.
Standards version, data rights, and what this article does not claim
- Standards basis. GS1 2D Barcodes at Retail Point-of-Sale Implementation Guideline, release 1.1.0; GS1 retail solution provider 2D readiness; GS1 background on the 2027 date. Accessed 23 October 2026.
- Test data. Every payload in the matrix is synthetic.
00950600137562is a generated GTIN, not a real product; no retailer, brand or document holder data is reproduced here. The labels used to produce the images were generated by the Codepool GS1 generator, so the fixtures can be regenerated by anyone. - Code status. The JavaScript parser was run against all 12 matrix cases on 23 October 2026. It implements the AI table for the AIs used here, not the complete GS1 table — extend
FIXEDandVARIABLEbefore production use, and keep the “unknown AI” path failing closed. - Not claimed. This article does not claim ISO/IEC barcode verification, GS1 certification of any implementation, or that any particular scanner hardware will read a given pack. Those are separate tests with separate evidence.
Source Code
The complete sample includes the Canvas checkout, the GTIN and Digital Link parser, five guided scenarios, synthetic fixtures and the tests that check each expected cart decision.