Skip to content

GS1 Identifier Validation for FSMA 204 KDEs

A supplier feed rarely lies about its identifiers in an obvious way. It lies quietly: a Global Location Number arrives with thirteen digits, passes a naive length check, and gets written into a Key Data Element as a trusted location_id — except one digit was transposed in transit and the number now points at no facility that has ever existed. Under Supplier Data Ingestion, every GS1 key that crosses the boundary carries this risk. GLNs identify the ship-from and receive-to locations FSMA 204 requires, GTINs anchor product identity, and SSCCs tie a physical pallet to a shipping event. When any of these is structurally wrong, the corruption is silent: the record commits, the lot chain looks intact, and the defect only surfaces during a recall traceback when the identifier resolves to nothing.

This page owns one narrow guarantee inside the ingestion pipeline: no GS1 key is trusted in a Critical Tracking Event until it is proven structurally valid. That guarantee sits upstream of, and complements, the Schema Validation Rules gate — schema validation confirms a location_id is a non-empty string of the right shape, but only GS1 validation confirms that the string is a mathematically sound GLN whose check digit closes. The two run in sequence, and the mapping of each identifier to the KDE it populates is governed by the KDE Field Mapping Guide. Here we define the GS1 key formats, the modulo-10 check-digit algorithm that every one of them shares, where validation sits in ingestion, and a runnable pydantic v2 implementation that quarantines any identifier that fails.

GS1 Validation in the Ingestion Flow

GS1 validation is a structural pre-flight, not a lookup. It answers a cheaper, more fundamental question than “does this location exist?” — it answers “is this string even a well-formed GS1 key?” That distinction matters because the two checks have wildly different costs and failure modes. Structural validation is pure arithmetic: length, numeric composition, and a check-digit recomputation, all executable in microseconds with no network call. A registry cross-check is I/O, subject to latency and transient failure. Running the cheap structural gate first means a transposed or truncated identifier is rejected before it ever consumes a registry round-trip, and the far rarer case of a structurally perfect but unregistered key is handled separately.

The pipeline places the gate immediately after payload normalization and before the schema-validation model binds the value into a KDE. A raw identifier flows through four stages. First, a length and format check confirms the key is entirely numeric and carries the exact digit count its type mandates. Second, the modulo-10 check digit is recomputed from the payload digits and compared against the transmitted final digit; a mismatch means at least one digit was altered in transit or entry. Third, a registry cross-check — optional and configurable per deployment — confirms the structurally valid key resolves to a known GS1 company prefix or an internal location registry entry. Fourth, the record is either accepted into the KDE contract or routed to quarantine with its precise failure reason preserved. GS1 is the standards body that defines these keys and their check-digit rules; FSMA 204 is what obligates a facility to carry them as KDE Field Mapping Guide location and product elements.

Figure — GS1 identifier validation pipeline:

GS1 identifier validation pipeline A raw GS1 identifier enters a four-stage pipeline. Stage one checks length and numeric format, stage two recomputes the modulo-10 check digit, and stage three cross-checks the key against a company-prefix or location registry. A key that passes every stage is accepted into the Key Data Element contract. A key that fails length, check digit, or the registry lookup drops into a shared quarantine store that records the failing stage and the original value. ok ok ok ok fail fail fail Raw identifier GLN · GTIN · SSCC Length / format numeric · digit count Mod-10 check recompute digit Registry check prefix · location Accept as KDE trusted identifier Quarantine failing stage · original value
Cheap arithmetic gates run before any registry I/O; a failure at any stage quarantines the key with its reason.

GS1 Key Formats and the KDEs They Populate

Three GS1 keys carry most of the identifier load in an FSMA 204 pipeline, and each has a fixed length and a trailing check digit. The Global Location Number (GLN) is thirteen digits and identifies a party or a physical place — the ship-from and receive-to locations a Critical Tracking Event must record. The Global Trade Item Number in its GTIN-14 form is fourteen digits and identifies a trade item at a packaging level; its leading digit is an indicator that distinguishes packaging tiers of the same base product. The Serial Shipping Container Code (SSCC) is eighteen digits and identifies an individual logistics unit — a specific pallet or case — with a leading extension digit assigned by the brand owner. In every case the final digit is a modulo-10 check digit computed from all preceding digits.

The regulatory column below cites where FSMA 204 obligates the location or product the identifier stands for; the digit-count and check-digit rules themselves are defined by GS1 as the standards body, not by the CFR. You can review the canonical GLN specification at the GS1 GLN standard.

GS1 key Length Check digit Populates KDE Regulatory Source (21 CFR Part 1, Subpart S)
GLN 13 digits Position 13 (mod-10) location_id (ship-from / receive-to) § 1.1340 / § 1.1345 (location of the CTE); GS1 defines the key
GTIN-14 14 digits Position 14 (mod-10) product identifier crosswalk § 1.1340(a) (product description / identity); GS1 defines the key
SSCC 18 digits Position 18 (mod-10) logistics-unit reference on a shipping event § 1.1340(a) (shipping KDEs); GS1 defines the key

The leading digits carry structure worth validating on their own. A GTIN-14 indicator digit of 0 conventionally denotes the base item, while 18 denote fixed packaging tiers and 9 denotes a variable-measure item — a value outside 09 is impossible, and pipelines that map packaging tiers should confirm the indicator falls in the expected range for that supplier. The SSCC extension digit, likewise 09, is assigned to increase serial capacity and should be numeric and present. Neither leading digit changes the check-digit computation, which always spans every digit except the last, but both are cheap structural signals that catch a malformed key before it reaches a registry.

Production GS1 Validators

The implementation below builds every validator on one shared modulo-10 function so the arithmetic is defined once and reused. The GS1 modulo-10 algorithm weights digits alternately by 3 and 1, reading from the rightmost payload digit leftward, sums the weighted products, and the check digit is the amount needed to round that sum up to the next multiple of ten. The three pydantic v2 models differ only in length and in the leading-digit rules they enforce; each rejects a bad key by raising, and the batch driver routes any rejected identifier to a durable quarantine store with structured logging.

from __future__ import annotations

import logging
from enum import Enum

from pydantic import BaseModel, ConfigDict, field_validator

gs1_logger = logging.getLogger("fsma204.gs1")
gs1_logger.setLevel(logging.INFO)


def mod10_check_digit(payload_digits: str) -> int:
    """Compute the GS1 modulo-10 check digit for the payload (all digits but the last).

    Weights alternate 3, 1 from the rightmost payload digit leftward. The check
    digit is whatever value raises the weighted sum to the next multiple of ten.
    """
    total = 0
    for position, char in enumerate(reversed(payload_digits)):
        weight = 3 if position % 2 == 0 else 1
        total += int(char) * weight
    return (10 - (total % 10)) % 10


def is_valid_gs1_key(value: str, length: int) -> bool:
    """Structural GS1 check: exact length, all numeric, closing check digit."""
    if len(value) != length or not value.isdigit():
        return False
    expected = mod10_check_digit(value[:-1])
    return expected == int(value[-1])


class GS1KeyType(str, Enum):
    GLN = "GLN"
    GTIN_14 = "GTIN-14"
    SSCC = "SSCC"


class GLNIdentifier(BaseModel):
    model_config = ConfigDict(strict=True)
    value: str

    @field_validator("value")
    @classmethod
    def check_gln(cls, v: str) -> str:
        if not is_valid_gs1_key(v, 13):
            raise ValueError("GLN must be 13 numeric digits with a valid mod-10 check digit")
        return v


class GTIN14Identifier(BaseModel):
    model_config = ConfigDict(strict=True)
    value: str

    @field_validator("value")
    @classmethod
    def check_gtin(cls, v: str) -> str:
        if not is_valid_gs1_key(v, 14):
            raise ValueError("GTIN-14 must be 14 numeric digits with a valid mod-10 check digit")
        if v[0] not in "0123456789":  # indicator digit sanity
            raise ValueError("GTIN-14 indicator digit must be 0-9")
        return v


class SSCCIdentifier(BaseModel):
    model_config = ConfigDict(strict=True)
    value: str

    @field_validator("value")
    @classmethod
    def check_sscc(cls, v: str) -> str:
        if not is_valid_gs1_key(v, 18):
            raise ValueError("SSCC must be 18 numeric digits with a valid mod-10 check digit")
        return v


_MODELS: dict[GS1KeyType, type[BaseModel]] = {
    GS1KeyType.GLN: GLNIdentifier,
    GS1KeyType.GTIN_14: GTIN14Identifier,
    GS1KeyType.SSCC: SSCCIdentifier,
}


def validate_gs1_batch(
    candidates: list[tuple[GS1KeyType, str]],
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
    """Validate a batch of (key_type, value) pairs; return (accepted, quarantined)."""
    accepted: list[dict[str, str]] = []
    quarantined: list[dict[str, str]] = []

    for key_type, value in candidates:
        model = _MODELS[key_type]
        try:
            model(value=value)
        except ValueError as exc:
            reason = str(exc)
            quarantined.append({"key_type": key_type.value, "value": value, "reason": reason})
            gs1_logger.warning(
                "GS1_REJECTED | type=%s | value=%s | reason=%s", key_type.value, value, reason
            )
            continue
        accepted.append({"key_type": key_type.value, "value": value})
        gs1_logger.info("GS1_ACCEPTED | type=%s | value=%s", key_type.value, value)

    return accepted, quarantined

The strict=True configuration blocks pydantic from silently coercing an integer or a whitespace-padded string into a plausible-looking identifier, so a supplier that transmits a GLN as a JSON number rather than a string is surfaced rather than accepted. Because mod10_check_digit is defined once and consumed by all three keys through is_valid_gs1_key, the arithmetic cannot drift between identifier types — a property that matters when an auditor asks why a specific SSCC was rejected and you need one authoritative code path to point at. The batch driver isolates each candidate, so a single malformed identifier never aborts the validation of the rest.

Error Handling and Quarantine Strategy

A GS1 rejection is deterministic. A number that fails its check digit fails it on every retry, because the arithmetic is fixed — there is no transient condition under which a broken key suddenly closes to its transmitted check digit. The validators therefore never retry a structural failure; they route it straight to quarantine. This mirrors the fault classification the Schema Validation Rules gate applies, where deterministic validation faults and transient transport faults are handled on separate paths, and it means the two gates present a consistent quarantine contract to the operator.

Quarantine preserves the exact rejected value alongside the failing stage. The distinction between a length failure, a check-digit failure, and a registry-miss failure is diagnostically load-bearing: a length failure usually means a truncated field or a column-mapping error in the parser, a check-digit failure almost always means a transposed or mistyped digit, and a registry miss on a structurally valid key means the identifier is well-formed but unknown — often a newly issued GLN not yet synced from the supplier’s master data. Routing these to distinct remediation queues turns a raw rejection count into an actionable signal. When rejections concentrate on one supplier, that anomaly should surface to Data Quality Monitoring rather than accumulate silently, and the dead-letter reconciliation reuses the shared Error Handling Workflows.

Integration with the Ingestion Pipeline

Within the parent Supplier Data Ingestion pipeline, GS1 validation is a focused pre-flight that runs after normalization and before the schema gate binds identifiers into KDEs. A record arriving through the CSV/EDI Parser Setup or a REST vector under API Polling Strategies converges on the same GS1 check, so a GLN from a flat file and a GLN from an API payload are held to identical structural standards. Once the identifier is proven valid it flows into the Schema Validation Rules model, where it populates the location_id or product-crosswalk KDE the KDE Field Mapping Guide defines.

New vendors are wired to this gate through Supplier Onboarding Automation, which registers each supplier’s GS1 company prefix and the location registry the cross-check consults before the first live batch runs. That registration is what makes the fourth pipeline stage meaningful: without a per-supplier prefix map, a registry cross-check can only confirm the key is well-formed, not that it belongs to a party the facility actually trades with.

Operational Notes

Run the validators as part of the same triggered ingestion job that hosts schema validation — an object-storage event handler or a queue-consumer task — so a structurally invalid identifier is caught in the same pass that normalizes the record. Recommended runtime and dependencies:

  • Python 3.10+ (the code uses from __future__ import annotations, list[...] / dict[...] generics, and X | Y unions).
  • pydantic ≥ 2.5 — the v2 field_validator / ConfigDict API. Never mix in the v1 validator decorator.

Configuration belongs in the environment. Provide a GS1_REGISTRY_ENDPOINT for the optional cross-check, a QUARANTINE_STORE target shared with the schema gate, and a per-supplier company_prefix_map loaded from onboarding. Keep the check-digit implementation under version control and covered by unit tests against published GS1 examples, so a well-meaning refactor of the weighting loop cannot silently invert the 3-1 pattern. The two procedural walkthroughs — Validate GLN Check Digits and Validate GTIN & SSCC — carry the test suites that lock this behavior in.

Frequently Asked Questions

Why validate a GS1 check digit if the field already passes a length check?

A length check confirms only that the right number of characters arrived; it says nothing about whether those characters are correct. A single transposed digit keeps the length identical but breaks the modulo-10 check digit, so a GLN like a transposed pair still measures thirteen digits while pointing at no real location. The check digit is the cheapest arithmetic guard that catches this class of silent corruption before the identifier is trusted in a KDE.

Is GS1 validation a substitute for the schema validation gate?

No; they answer different questions and run in sequence. Schema validation confirms a value is a non-empty string of the right shape and type for its KDE, while GS1 validation proves the string is a mathematically sound GS1 key whose check digit closes. A value can pass schema validation and still be a structurally invalid GLN, so GS1 validation runs first and hands only proven identifiers to the schema model.

Does FSMA 204 require GS1 identifiers specifically?

FSMA 204 requires that Critical Tracking Events record location and product information as Key Data Elements, and it permits GS1 keys such as the GLN as an acceptable way to express a location identifier. GS1 is the standards body that defines the key formats and their check-digit rules; the CFR obligates the underlying KDE. The mapping table on this page cites the CFR section for each KDE and notes that GS1 defines the key itself.

Why run the structural check before the registry cross-check?

Structural validation is pure arithmetic that costs microseconds and needs no network call, while a registry cross-check is I/O subject to latency and transient failure. Running the cheap gate first rejects transposed or truncated keys before they consume a registry round-trip, and it isolates the rarer case of a structurally perfect but unregistered key onto its own remediation path.

What distinguishes a quarantined GS1 key from a dropped one?

A quarantined key is never discarded; the pipeline writes the exact rejected value, its key type, and the failing stage to a durable store so the record can be reconciled. The failing stage is diagnostically important because a length failure points at a parser mapping error, a check-digit failure points at a transposed digit, and a registry miss points at master data that has not yet synced. Distinct reasons route to distinct remediation queues.

Why share a single modulo-10 function across all three validators?

Defining the arithmetic once and reusing it through a shared helper means the weighting pattern cannot drift between GLN, GTIN, and SSCC validation. If each validator carried its own copy of the loop, a refactor could silently invert the 3-1 weights in one place and not another, producing inconsistent acceptances. One authoritative code path also gives an auditor a single function to inspect when asking why a specific identifier was rejected.

Up: Supplier Data Ingestion & Sync Automation — this gate proves every GS1 key structurally sound before the parent pipeline trusts it in a KDE.