> ## 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.

# Decode GS1 AI

> Parse a GS1 Application Identifier (AI) element string — bracketed or unbracketed — into structured key/value pairs, distinguishing fixed-length AIs (GTIN, dates) from variable-length AIs that require an FNC1 separator.

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

```python theme={null}
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)ABC123`  → **bracketed**
2. `010950600013435217270101\x1d10ABC123`     → **unbracketed**
3. `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

```python theme={null}
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.

```python theme={null}
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}/…`

```python theme={null}
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:

```json theme={null}
{
  "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.

## Related skills

* `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).
