Skip to main content

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

EncodingExampleWhen you see it
Bracketed (human readable)(01)00614141000005(10)A123(21)SN9999EDI documents, marketing copy, debug output.
Unbracketed (machine readable)010061414100000510A123\x1d21SN9999Actual 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:
  1. (01)09506000134352(17)270101(10)ABC123bracketed
  2. 010950600013435217270101\x1d10ABC123unbracketed
  3. https://id.gs1.org/01/09506000134352/10/ABC123Digital 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.
AILength (after AI)FieldNotes
0018SSCCSerial Shipping Container Code.
0114GTINAlways zero-padded to GTIN-14 internally.
0214GTIN of contained trade itemsFor logistics labels.
116Production date (YYMMDD)
126Due date (YYMMDD)
136Packaging date (YYMMDD)
156Best-before date (YYMMDD)
166Sell-by date (YYMMDD)
176Expiration date (YYMMDD)Day 00 means “end of month”.
202Variant number
80056Price per unit of measure
800618GCTIN (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.
AIMax lengthField
1020Batch / lot number
2120Serial number
2220Consumer product variant
24030Additional product identification
24130Customer part number
2426Made-to-order variation
308Variable count
378Count of trade items
40030Customer purchase order
40130Consignment number (GINC)
40217Shipment identification (GSIN)
40330Routing code
41041713Trading-partner location codes
42020Ship-to postal code (single postal authority)
42112Ship-to postal code + ISO country code
8003up to 30GRAI (Global Returnable Asset Identifier)
8004up to 30GIAI (Global Individual Asset Identifier)
801112Component / part serial
802025Payment slip reference
909930Internal 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:
  1. 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).
  2. If the AI is fixed-length, slice length characters as the value.
  3. If it’s variable-length, slice up to the next \x1d (or end of string, if there’s no separator).
  4. 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).
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:
  1. 01 → fixed 14 → 09506000134352 (GTIN).
  2. 17 → fixed 6 → 300901 (expiry 2030-09-01).
  3. 10 → variable, up to FNC1 → 45454GH (lot).
  4. 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).