Registering GLNs for DSCSA Trading Partners

Before a trading partner’s EPCIS events — or their answers to your Verification Router Service queries — can be trusted, the location identifiers attached to those events must resolve to a facility you actually recognize. This guide is part of Trading Partner Verification & Onboarding, the onboarding stage of DSCSA Compliance Architecture & Standards Mapping, and it covers the specific mechanics of registering, validating, and managing GS1 Global Location Numbers (GLNs) for every manufacturer, repackager, wholesale distributor, and dispenser you exchange data with. Get GLN onboarding wrong and you inherit two failure modes at once: a readPoint or bizLocation value you cannot resolve to a real facility, and a verification response you cannot legally attribute to anyone.

A GLN is a 13-digit GS1 identifier for a party or a physical location — a distribution center, a packaging line, a pharmacy counter. Trading partners quote their GLN during onboarding, print it (encoded) on shipping labels, and reference it in every EPCIS event they emit. If you accept events referencing an unregistered or malformed GLN, you are recording traceability history against a location that does not exist in your master data, which is exactly the kind of gap an auditor or a suspect-product investigation surfaces later, when it is expensive to fix.

Prerequisites

  • Python 3.10+ — the snippets use str | None unions, Literal types, and dataclass-style Pydantic models.
  • Pydantic v2 — for field_validator-based structural validation of GLNs and partner records.
  • A partner-onboarding record for each trading partner: legal entity name, the GLN(s) they have quoted for their locations, their role in the supply chain, and the GS1 Company Prefix length GS1 assigned them (this is not derivable from the digits alone — see Gotchas).
  • A master-data store (a database table in production; the in-memory registry below models the same contract) keyed by GLN, so every inbound EPCIS event can be checked against a known set of trading-partner locations before it is trusted.
  • Familiarity with EPCIS readPoint/bizLocation — this guide assumes events already conform to the structural rules described in step-by-step EPCIS 2.0 event formatting; here we validate one field within that structure, not the whole event.
Structure of a 13-digit GS1 GLN A GLN is 13 digits split into a GS1 Company Prefix of variable length, a Location Reference that fills the remainder to 12 digits, and a single GS1 modulo-10 check digit computed over the first 12 digits. 13 DIGITS TOTAL GS1 Company Prefix 6-10 digits, assigned by GS1 Location Reference fills remainder to 12 digits Check digit first 12 digits — input to GS1 modulo-10 weighted sum → check digit

Step-by-Step Solution

Step 1 — Validate GLN format and check digit

A GLN uses the same GS1 modulo-10 check-digit algorithm as a GTIN, applied to 13 digits instead of 14: alternating weights of 3 and 1, read from the rightmost digit of the 12-digit body. Reject anything that fails structurally before it ever touches master data.

def gln_check_digit_ok(gln: str) -> bool:
    """GS1 modulo-10 check digit over the first 12 digits of a 13-digit GLN."""
    body, check = gln[:-1], int(gln[-1])
    total = sum(int(d) * (3 if i % 2 == 0 else 1)
               for i, d in enumerate(reversed(body)))
    return (10 - total % 10) % 10 == check

DSCSA/GS1 note: a GLN that fails this check is not a data-quality nuance — it means the readPoint/bizLocation on every event it appears in cannot be trusted, so it must be rejected structurally, not coerced or truncated.

Step 2 — Wrap the rule in a Pydantic v2 model

Encode the format and check-digit rules as a field_validator so every place a GLN enters the system — onboarding forms, EPCIS ingestion, VRS partner directories — validates identically.

from pydantic import BaseModel, field_validator


class GLN(BaseModel):
    value: str

    @field_validator("value")
    @classmethod
    def _valid_gln(cls, v: str) -> str:
        if len(v) != 13 or not v.isdigit():
            raise ValueError("GLN must be 13 numeric digits")
        if not gln_check_digit_ok(v):
            raise ValueError("GLN check digit failed GS1 modulo-10")
        return v

DSCSA/GS1 note: keeping the GLN as a str, never an int, preserves leading zeros — a GS1 Company Prefix commonly starts with 0, and casting to a number silently corrupts the identifier.

Step 3 — Model the partner-GLN registry

A registered GLN is only useful alongside the partner metadata that gives it meaning: whose location it is, what role they play, and whether onboarding has actually been confirmed. The company_prefix_length field matters because GS1 assigns prefixes of varying length (6-10 digits) — it must come from the partner’s registered profile, not be guessed from the digit string.

from datetime import datetime
from typing import Literal
from pydantic import BaseModel, field_validator

PartnerRole = Literal[
    "manufacturer", "repackager", "wholesale_distributor",
    "dispenser", "third_party_logistics",
]


class TradingPartnerGLN(BaseModel):
    gln: str
    partner_name: str
    role: PartnerRole
    company_prefix_length: int
    verified: bool = False
    registered_at: datetime

    @field_validator("gln")
    @classmethod
    def _gln_valid(cls, v: str) -> str:
        if len(v) != 13 or not v.isdigit():
            raise ValueError("GLN must be 13 numeric digits")
        if not gln_check_digit_ok(v):
            raise ValueError("GLN check digit failed GS1 modulo-10")
        return v

    @field_validator("company_prefix_length")
    @classmethod
    def _prefix_len(cls, v: int) -> int:
        if not (6 <= v <= 10):
            raise ValueError("GS1 Company Prefix is 6-10 digits")
        return v

    @property
    def company_prefix(self) -> str:
        return self.gln[: self.company_prefix_length]

    @property
    def location_reference(self) -> str:
        return self.gln[self.company_prefix_length:-1]


class GLNRegistry:
    """Master-data store of registered trading-partner locations."""

    def __init__(self) -> None:
        self._by_gln: dict[str, TradingPartnerGLN] = {}

    def register(self, partner: TradingPartnerGLN) -> None:
        existing = self._by_gln.get(partner.gln)
        if existing is not None and existing.partner_name != partner.partner_name:
            raise ValueError(
                f"GLN {partner.gln} is already registered to {existing.partner_name}"
            )
        self._by_gln[partner.gln] = partner  # GLN is the natural idempotency key

    def get(self, gln: str) -> TradingPartnerGLN | None:
        return self._by_gln.get(gln)

DSCSA/GS1 note: using the GLN itself as the dictionary key makes re-registration idempotent — a retried onboarding request for the same partner updates the record instead of creating a duplicate.

Step 4 — Know when you need the GLN and when you need the SGLN

The 13-digit GLN is the master-data form: what appears on a trading partner agreement, in the GS1 Global Registry, and in your registry above. Inside an EPCIS event, readPoint and bizLocation instead carry a Serialized GLN (SGLN) — an EPC URI that decomposes the same company prefix and location reference and adds an extension component for a specific sub-location.

Aspect GLN (master data) SGLN (EPCIS readPoint/bizLocation)
Form 13-digit number with an embedded check digit URI: urn:epc:id:sgln:<CompanyPrefix>.<LocationRef>.<Extension>
Where it lives Partner onboarding records, VRS directories, GS1 Global Registry ObjectEvent/AggregationEvent readPoint and bizLocation fields
Granularity One party or site Optionally a sub-location — a dock door, a freezer, a storage bin — via the extension
Check digit Present, as the last digit Absent — must be recomputed from the prefix and location reference if you need the GLN back
def to_sgln_uri(company_prefix: str, location_reference: str, extension: str = "0") -> str:
    """Build the SGLN URI EPCIS expects in readPoint/bizLocation.

    Extension "0" conventionally means "the location itself" — use a
    non-zero extension only when the partner has told you it identifies
    a specific sub-location, not an arbitrary site alias.
    """
    return f"urn:epc:id:sgln:{company_prefix}.{location_reference}.{extension}"


def sgln_to_gln13(company_prefix: str, location_reference: str) -> str:
    """Recompute the 13-digit GLN from an SGLN's prefix and location reference."""
    body = company_prefix + location_reference
    total = sum(int(d) * (3 if i % 2 == 0 else 1)
               for i, d in enumerate(reversed(body)))
    check = (10 - total % 10) % 10
    return f"{body}{check}"

DSCSA/GS1 note: because the SGLN URI never carries the check digit, the only way to confirm an incoming readPoint names a real, registered location is to recompute the GLN13 and look it up — which is exactly what event validation needs to do next. See the GS1 identification keys standard for the full GLN allocation rules.

Step 5 — Reject events from unregistered or unverified locations

Before an inbound EPCIS event is allowed to touch the repository — the same gate described in DSCSA Compliance Architecture & Standards Mapping, and enforced ahead of any Verification Router Service lookup — its readPoint must resolve to a partner you have onboarded and confirmed, not merely one that parses.

def extract_sgln_parts(sgln_uri: str) -> tuple[str, str, str]:
    prefix = "urn:epc:id:sgln:"
    if not sgln_uri.startswith(prefix):
        raise ValueError(f"not an SGLN URI: {sgln_uri}")
    parts = sgln_uri[len(prefix):].split(".")
    if len(parts) != 3:
        raise ValueError(f"malformed SGLN URI: {sgln_uri}")
    company_prefix, location_ref, extension = parts
    return company_prefix, location_ref, extension


def validate_event_read_point(event: dict, registry: GLNRegistry) -> TradingPartnerGLN:
    """Resolve an event's readPoint to a registered, verified trading partner."""
    read_point = event.get("readPoint", {}).get("id", "")
    company_prefix, location_ref, _extension = extract_sgln_parts(read_point)
    gln13 = sgln_to_gln13(company_prefix, location_ref)

    partner = registry.get(gln13)
    if partner is None:
        raise LookupError(f"readPoint GLN {gln13} is not a registered trading partner")
    if not partner.verified:
        raise PermissionError(
            f"GLN {gln13} ({partner.partner_name}) is registered but not yet verified"
        )
    return partner

DSCSA/GS1 note: separate the “not registered” and “registered but unverified” failures — the first is an onboarding gap to escalate to the partner-management team, the second means a trading partner agreement is still pending and events should be quarantined, not silently dropped, per the same discipline used for suspect-product investigation workflows.

Verification

Confirm both the check-digit math and the registry gate with a small pytest suite before wiring this into ingestion:

import pytest
from datetime import datetime, timezone

VALID_GLN = "1234567890128"  # company prefix "1234567" + location "89012" + check digit 8


def test_valid_gln_passes():
    assert gln_check_digit_ok(VALID_GLN) is True


def test_corrupted_check_digit_fails():
    assert gln_check_digit_ok(VALID_GLN[:-1] + "0") is False


def test_sgln_round_trips_to_registered_gln():
    registry = GLNRegistry()
    registry.register(TradingPartnerGLN(
        gln=VALID_GLN,
        partner_name="Example Wholesale Distributor",
        role="wholesale_distributor",
        company_prefix_length=7,
        verified=True,
        registered_at=datetime.now(timezone.utc),
    ))
    event = {"readPoint": {"id": to_sgln_uri("1234567", "89012")}}
    partner = validate_event_read_point(event, registry)
    assert partner.partner_name == "Example Wholesale Distributor"


def test_unregistered_read_point_is_rejected():
    registry = GLNRegistry()
    event = {"readPoint": {"id": to_sgln_uri("1234567", "89012")}}
    with pytest.raises(LookupError):
        validate_event_read_point(event, registry)

Run this suite in CI on every change to the partner registry schema, and separately reconcile your live registry against the GLNs your partners have actually confirmed in signed onboarding paperwork — a passing check digit only proves the number is well-formed, not that the partner agreement exists.

Gotchas & Edge Cases

  • Company Prefix length is not derivable from the digits. GS1 assigns prefixes of 6 to 10 digits per organization; you cannot split a bare GLN into prefix and location reference correctly without knowing the assigned length from the partner’s GS1 registration, not by guessing a fixed offset.
  • Never cast a GLN to int. A GS1 Company Prefix frequently starts with a leading zero; converting to a number strips it and every subsequent check-digit computation and registry lookup silently corrupts.
  • Extension “0” vs. no extension. Some trading partners emit SGLN URIs without an explicit extension, others always append .0. Normalize both to the same registry lookup key rather than treating a missing extension as a different location than extension 0.
  • Idempotency-key collisions on re-onboarding. A retried onboarding submission for the same partner should update the existing registry record, not create a second one under a slightly different partner_name — key the upsert on the GLN itself, since it is the one field guaranteed unique per location.
  • “Valid format” is not “authorized.” A GLN that passes the check digit can still belong to a partner whose trading partner agreement has not been signed. Keep verified as a distinct gate from structural validity, and reject events from unverified locations rather than merely flagging them.