The payload grammar.

Fixed vs variable termination, GS separators, AIM symbology prologues, date and decimal AI semantics — a pinned table, not folklore.

One label's worth of data, blown out.

rendered as a GS1-128 barcode — in the stripes, digits ride in pairs (one symbol per two digits); the label text above is the human-readable form of the same payload

(01) 0950600013435 product code the GTIN, minus its last digit — that one is the check
2 check the GTIN's own check digit — mod10 pass
(17) 260930 expires 30 Sep 2026 — DD=00 would mean end of month
(10) LOT42 batch / lot variable length — in the stripes it ends at a separator or at the end
GTIN   0950600013435 · check 2
sum     0×3 + 9×1 + 5×3 + 0×1 + 6×3 + 0×1 + 0×3 + 0×1 + 1×3 + 3×1 + 4×3 + 3×1 + 5×3 = 78
check   (10  78 mod 10) mod 10 = 2   → mod10 pass

The row.

schemegs1.element-string
grammarAn AI is a 2–4 digit tag followed by a typed value. Fixed-length AIs run straight into the next AI; variable-length AIs terminate at GS (0x1D) or end of payload. The tag/length/type table is pinned data derived from the GS1 General Specifications.
check digitStructural — carried values bring their own rails (AI 01 carries the GTIN mod-10). Errors are typed with byte offsets.
canonical formA typed AI map. generate emits deterministic AI ordering; resolve accepts any valid ordering.
symbologiesGS1-128 (]C1) · GS1 DataMatrix (]d2) · GS1 QR (]Q3) · and the wider AIM prologue set (]C0 ]E0 ]I1 ]A0 ]Q1 ]L2) — the prologue names the carrier, never the identifier
enrichment— (grammar layer; enrichment attaches to the carried keys)
join ruleThe AI map is the event's raw material: 01+21 → epcList instance, 01+10 → quantityList class, 00 → parentID.
pinned digestgs1.element-string sha256:6e4b22877ad1c3add56c885785f5768670f9c6310f15150dc69d3aff6294dac4
as data/schemes/ai.json

In practice.

The grammar is exactly why one parser covers both 2D payload forms: a GS1 element string and a Digital Link URI normalize to the same typed AI map, and the round-trip law — resolve(generate(x)) === x — is asserted over that map, not over bytes that include symbology framing.

On the blog.

  • GS1 Application Identifiers: the grammar inside the symbol

    AIs are the typed field system of the physical world — (01) GTIN, (10) lot, (17) expiry, (21) serial, (00) SSCC — and an FNC1-delimited element string is a binary wire format with parsing rules, not "text in a barcode." Parsed here byte by byte, from the AIM prefix to the last variable-length field.

  • What did I just scan? Payload identification from the first three bytes

    A scanner hands you bytes, not meaning. The AIM symbology identifier, the FNC1 position, Code-39 framing, and URI shape classify any payload deterministically — worked here on ten raw payloads, the way resolve does it, from a pinned decision table rather than folklore.

  • Element string or Digital Link URI: the encoding decision Sunrise leaves to you

    A 2D symbol can carry the same GTIN+lot+expiry as an FNC1 element string or as a GS1 Digital Link URI. One resolves in a consumer's phone, one does not — and the choice belongs to the GTIN owner, not to the Sunrise 2027 program. The decision matrix, worked with generate.

  • resolve(generate(x)) === x: generation and resolution are one function, tested

    The write side and the read side of an identifier layer must be exact inverses — every generated element string, Digital Link URI, and rendered symbol must resolve back to the identical canonical identity — and that law is a CI gate over a published golden corpus, not a design preference. The harness, the corpus split, and how to check the claim yourself.

Get an API key.

The local verbs run from npx barcoding.dev today with no key. A key opens the network half of the verb set.