Documentation Index
Fetch the complete documentation index at: https://docs.closient.com/llms.txt
Use this file to discover all available pages before exploring further.
GS1 barcodes carry data as a concatenation of Application Identifier
(AI) + value pairs. AIs are numeric prefixes (2-4 digits) that announce
what the next value means: 01 = GTIN, 10 = batch/lot, 21 =
serial, 00 = SSCC, 8003 = GRAI, and so on. This skill teaches an
agent how to take an element string off a barcode scan and unpack it
into structured data the rest of the system can query.
When to use
- A scanner / OCR / EDI feed produced a raw element string and you need
structured fields (GTIN, lot, expiry, serial, SSCC, GRAI, GIAI).
- The agent received a GS1 Digital Link (
https://id.gs1.org/01/...)
and needs to extract the AIs the path encodes.
- A user pasted a GS1-128 barcode payload and asked “what does this
mean?”.
For the inverse — building an AI element string or Digital Link URL
from structured data — see build-gs1-digital-link.
The two encodings
| Encoding | Example | When you see it |
|---|
| Bracketed (human readable) | (01)00614141000005(10)A123(21)SN9999 | EDI documents, marketing copy, debug output. |
| Unbracketed (machine readable) | 010061414100000510A123\x1d21SN9999 | Actual barcode payload (GS1-128, DataMatrix, QR with GS1 framing). |
Both encode the same data; bracketed form just makes the AI boundaries
visible. The unbracketed form uses FNC1 (\x1d, also written
<GS> or ASCII 29) to mark the end of a variable-length value.
Step 1 — Detect the variant
def detect_variant(s: str) -> str:
if s.startswith("("):
return "bracketed"
if s.startswith("https://") or s.startswith("http://"):
return "digital_link"
return "unbracketed"
Three real shapes to expect:
(01)09506000134352(17)270101(10)ABC123 → bracketed
010950600013435217270101\x1d10ABC123 → unbracketed
https://id.gs1.org/01/09506000134352/10/ABC123 → Digital Link path
Step 2 — Know the fixed-length AIs
These AIs have a predefined length; the parser jumps that many
characters and continues. No separator needed.
| AI | Length (after AI) | Field | Notes |
|---|
00 | 18 | SSCC | Serial Shipping Container Code. |
01 | 14 | GTIN | Always zero-padded to GTIN-14 internally. |
02 | 14 | GTIN of contained trade items | For logistics labels. |
11 | 6 | Production date (YYMMDD) | |
12 | 6 | Due date (YYMMDD) | |
13 | 6 | Packaging date (YYMMDD) | |
15 | 6 | Best-before date (YYMMDD) | |
16 | 6 | Sell-by date (YYMMDD) | |
17 | 6 | Expiration date (YYMMDD) | Day 00 means “end of month”. |
20 | 2 | Variant number | |
8005 | 6 | Price per unit of measure | |
8006 | 18 | GCTIN (component) | |
Step 3 — Know the variable-length AIs
These accept up to a maximum number of characters and must be
terminated by FNC1 (\x1d) when followed by another AI. If the
variable-length value is the last element in the string, no separator
is needed.
| AI | Max length | Field |
|---|
10 | 20 | Batch / lot number |
21 | 20 | Serial number |
22 | 20 | Consumer product variant |
240 | 30 | Additional product identification |
241 | 30 | Customer part number |
242 | 6 | Made-to-order variation |
30 | 8 | Variable count |
37 | 8 | Count of trade items |
400 | 30 | Customer purchase order |
401 | 30 | Consignment number (GINC) |
402 | 17 | Shipment identification (GSIN) |
403 | 30 | Routing code |
410–417 | 13 | Trading-partner location codes |
420 | 20 | Ship-to postal code (single postal authority) |
421 | 12 | Ship-to postal code + ISO country code |
8003 | up to 30 | GRAI (Global Returnable Asset Identifier) |
8004 | up to 30 | GIAI (Global Individual Asset Identifier) |
8011 | 12 | Component / part serial |
8020 | 25 | Payment slip reference |
90–99 | 30 | Internal applications |
Step 4 — Parse, bracketed → structured
import re
BRACKETED_RE = re.compile(r"\((\d{2,4})\)([^(]*)")
def parse_bracketed(s: str) -> dict[str, str]:
return dict(BRACKETED_RE.findall(s))
parse_bracketed("(01)09506000134352(17)270101(10)ABC123")
# {'01': '09506000134352', '17': '270101', '10': 'ABC123'}
Trivial — the brackets do the framing work for you.
Step 5 — Parse, unbracketed → structured
This is where most agents trip up. The algorithm:
- Read 2-4 leading digits and try them against the AI table. Try the
longest prefix that matches a known AI (so
8003 is preferred over
80 + 03).
- If the AI is fixed-length, slice
length characters as the value.
- If it’s variable-length, slice up to the next
\x1d (or end of
string, if there’s no separator).
- Repeat from the position after the value.
FIXED = {"00":18, "01":14, "02":14, "11":6, "12":6, "13":6,
"15":6, "16":6, "17":6, "20":2, "8005":6, "8006":18}
VAR_MAX = {"10":20, "21":20, "22":20, "240":30, "241":30,
"242":6, "8003":30, "8004":30}
FNC1 = "\x1d"
def parse_unbracketed(s: str) -> dict[str, str]:
out, i = {}, 0
while i < len(s):
# try longest AI prefix first (4 → 2 chars)
for n in (4, 3, 2):
ai = s[i:i+n]
if ai in FIXED or ai in VAR_MAX:
i += n
break
else:
raise ValueError(f"Unknown AI at offset {i}: {s[i:i+4]!r}")
if ai in FIXED:
length = FIXED[ai]
out[ai] = s[i:i+length]
i += length
else:
end = s.find(FNC1, i)
end = len(s) if end == -1 else end
out[ai] = s[i:end]
i = end + (1 if end < len(s) else 0)
return out
Production parsers should also:
- Normalize bracketed input by stripping brackets and re-emitting an
unbracketed string with FNC1 separators inserted, then run the
unbracketed algorithm. One code path is easier to maintain than two.
- Validate the Mod-10 check digit on AI
01 (GTIN). The Closient
apps.core.types.GTIN.from_raw() does this — reuse if you’re in
Python.
- Accept the literal characters
^ or ~ as FNC1 sentinels (some
scanners emit those instead of \x1d).
Step 6 — Parse, GS1 Digital Link → structured
Digital Link path is path-based AI encoding: /{AI}/{value}/{AI}/{value}/…
import re
from urllib.parse import urlparse
PATH_SEG_RE = re.compile(r"/(\d{2,4})/([^/]+)")
def parse_digital_link(url: str) -> dict[str, str]:
parts = PATH_SEG_RE.findall(urlparse(url).path)
return dict(parts)
parse_digital_link("https://id.gs1.org/01/09506000134352/10/ABC123/21/SN9999")
# {'01': '09506000134352', '10': 'ABC123', '21': 'SN9999'}
Closient’s own resolver (apps.products.gs1_digital_link.parse_gs1_digital_link)
returns a typed dataclass with gtin, lot_number, expiry_date, and
serial_number already broken out — call that directly if you’re
running inside the monorepo.
Step 7 — Validate
Beyond the AI table, two checks matter:
- GTIN check digit (AI 01). Mod-10; the last digit is computed from
the first 13. Reject silently-malformed scans rather than continuing
with a corrupt GTIN.
- Date plausibility (AI 17 in particular).
YYMMDD with MM in
01-12, DD in 00-31 (where 00 is sentinel for “last day of
month”). The Closient scanner emits structured freshness_meta for
AI 17 — see apps.scanner.services.gs1_dates.
Example end-to-end
Input (off a meat-case label):
0109506000134352173009011045454GH\x1d21SN0001
Steps:
01 → fixed 14 → 09506000134352 (GTIN).
17 → fixed 6 → 300901 (expiry 2030-09-01).
10 → variable, up to FNC1 → 45454GH (lot).
21 → variable, no separator left → SN0001 (serial).
Structured:
{
"01": "09506000134352",
"17": "300901",
"10": "45454GH",
"21": "SN0001"
}
After that, Closient endpoints to call next:
GET /products/api/v1/products?gtin=09506000134352
GET /resolver/api/v1/verify/01/09506000134352/21/SN0001
Common mistakes
- Forgetting FNC1 after a variable-length AI before another AI
starts — produces a corrupted parse where
10ABC12321SN0001 is read
as a 20-character lot ending in 21SN0001.
- Not zero-padding GTIN-8 or GTIN-12 to GTIN-14 before lookups —
Closient stores GTINs as GTIN-14 internally; pad with leading zeros.
- Treating
(01) from a Digital Link query string as the SSCC AI 00
because the parser was eager about 2-digit prefixes; always try the
longest AI prefix first.
- Trusting
YY as 4-digit year. GS1 says YY in 00-49 → 20YY,
50-99 → 19YY (sliding window). Don’t hardcode the century.
build-gs1-digital-link — inverse: given GTIN + AIs, emit a valid
Digital Link URI.
resolve-gtin — fetch product detail once you have the GTIN.
check-product-availability — find local stock for a GTIN.
capture-epcis-event — once you have GTIN + lot + serial, record a
supply-chain event.
closient-api-quickstart — authenticate before calling any of the
above.
Authoritative references
- GS1 General Specifications (v25 or later) — section 3 (AIs) and
section 7 (data carriers).
- GS1 Digital Link Standard 1.4.
- ISO/IEC 15418 — GS1 Application Identifiers.
- Closient internal:
backend/apps/products/gs1_digital_link.py (parser)
and backend/apps/core/types/_gs1.py (typed value objects).