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.
The GS1 Digital Link standard (v1.4 / ISO/IEC 18975) encodes
Application Identifiers in URL paths instead of barcode element
strings, so the same identifier can drive a barcode scan, a deep
link, and a web resolver. This skill teaches an agent how to compose a
correct Digital Link URI from structured input — the inverse of
decode-gs1-ai.
When to use
- A consumer-facing surface needs a QR code or URL for a product:
packaging, shelf-talker, email receipt, branded resolver page.
- An ERP / PIM / TMS system has structured product+lot+serial data and
needs to emit GS1 Digital Link strings to label printers or partner
feeds.
- An agent received a raw GTIN and wants to construct the canonical
Closient resolver URL to hand back to a user.
For the inverse — taking a Digital Link URI and parsing it — see
decode-gs1-ai.
Anatomy of a Digital Link
https://id.gs1.org/01/09506000134352/10/ABC123/21/SN9999?linkType=gs1:pip
└──────┬──────┘ └──┬──────────────┘ └──┬─────┘ └──┬─────┘ └──────┬──────┘
host primary AI + value qualifier qualifier link type
| Component | Rules |
|---|
| Host | Any HTTPS host that supports the resolver protocol. id.gs1.org is the canonical GS1-managed host; you may use your own domain (e.g. id.closient.com, acme.gtin.one). |
| Primary identifier | Exactly one of /01/<gtin>, /00/<sscc>, /253/<gdti>, /255/<gcn>, /401/<ginc>, /402/<gsin>, /414/<gln>, /417/<gln>, /8003/<grai>, /8004/<giai>, /8006/<gctin>, /8010/<cpid>, /8013/<gmn>, /8017/<gsrn>, /8018/<gsrn>. |
| Qualifiers | Subsequent /{AI}/{value} segments that narrow the primary (e.g. /10/<lot>, /17/<expiry>, /21/<serial>). |
| Data attributes | Long-tail AIs go in the query string: ?15=271231 for AI 15 best-before. |
linkType= | Tells the resolver what kind of destination to serve (PIP, product page, instructions, recall, etc.). |
Before composing anything, normalize:
from apps.core.types.gtin import GTIN
gtin = str(GTIN.from_raw(raw_gtin)) # validates Mod-10 + pads to 14
assert lot is None or len(lot) <= 20
assert serial is None or len(serial) <= 20
# Expiry must be YYMMDD; convert ISO dates first.
Reject silently-malformed GTINs at this step rather than building a URI
that fails resolution downstream.
Step 2 — Compose the primary AI
GTIN is the common case:
host = "https://id.gs1.org"
path = f"/01/{gtin}" # gtin is 14 digits, zero-padded
url = host + path
For other primary identifiers:
| Primary identifier | Path |
|---|
| GTIN-14 | /01/<gtin14> |
| SSCC | /00/<sscc18> |
| GRAI (returnable asset) | /8003/<grai> |
| GIAI (individual asset) | /8004/<giai> |
| GLN (location) | /414/<gln> |
| GSIN (shipment) | /402/<gsin17> |
| GINC (consignment) | /401/<ginc> |
Step 3 — Append qualifier AIs (canonical order)
GS1 DL §4.4 fixes the order qualifier AIs must appear in: batch/lot
(10) before expiry (17) before serial (21) when all three are present
on a GTIN-anchored URL. The canonical order:
/01/{gtin}/22/{cpv}/10/{lot}/21/{serial}
Reordering is not a parse error but it is a canonicalization error —
two different orderings will produce two different canonical URLs and
some resolvers cache by canonical form.
Closient’s resolver normalizes the order on receipt, so an agent
emitting a non-canonical URL will get redirected to the canonical one.
Better to emit the canonical form from the start:
def append_qualifier(url: str, ai: str, value: str | None) -> str:
return f"{url}/{ai}/{value}" if value else url
url = host + f"/01/{gtin}"
url = append_qualifier(url, "10", lot)
url = append_qualifier(url, "17", expiry_yymmdd)
url = append_qualifier(url, "21", serial)
Step 4 — Encode special characters in values
GS1 Digital Link values are URL path segments, so anything outside the
unreserved set (A-Z a-z 0-9 - . _ ~) must be percent-encoded:
from urllib.parse import quote
safe_serial = quote(serial, safe="") # encode literally everything else
url += f"/21/{safe_serial}"
In practice, lot/serial characters that show up in real life and need
encoding: space (%20), + (%2B), & (%26), / (%2F), ?
(%3F), # (%23). Don’t pre-escape with \\ — that’s a different
encoding.
Step 5 — Attach data-attribute AIs (query string)
AIs that aren’t qualifiers go in the query string as <ai>=<value>:
/01/09506000134352?15=271231&3922=12.50
Useful examples:
| AI | What it means | Example query param |
|---|
| 15 | Best-before date YYMMDD | 15=271231 |
| 16 | Sell-by date YYMMDD | 16=271231 |
| 11 | Production date YYMMDD | 11=270101 |
| 8005 | Price per UoM | 8005=125000 |
| 3922 | Price (currency-implied) | 3922=12.50 |
Per the spec, query-string AIs should follow data-attribute conventions
— not used as primary IDs, never the basis for resolution decisions.
Step 6 — (Optional) attach linkType to drive resolution intent
A Digital Link URL can point at one identifier but many
destinations: product info page, recall notice, instructions, video,
loyalty enrollment. The linkType query string parameter tells the
resolver which destination the agent (or end user) wants:
https://id.gs1.org/01/09506000134352?linkType=gs1:pip
https://id.gs1.org/01/09506000134352?linkType=gs1:recallNotice
https://id.gs1.org/01/09506000134352?linkType=gs1:instructions
The IANA-registered link-type vocabulary is the source of truth (see
https://www.gs1.org/voc/). Closient’s resolver maps these to
hosted-page destinations and the Resolver model’s
destination_type column — see apps/resolver/gs1_link_types.py for
the exhaustive list this codebase recognizes.
If you don’t set linkType, the resolver returns its default
destination — typically the brand’s PIP or the public Closient
product page.
Step 7 — Pick the host
| Host | Use it when |
|---|
https://id.gs1.org | You want GS1’s official global resolver to handle the URL. |
https://id.closient.com | You’re publishing through Closient and want our resolver to serve hosted pages, redirects, and analytics. |
https://<brand>.gtin.one | Closient-managed branded resolver subdomain for a specific brand. |
https://<brand>.example.com | The brand’s own CNAME pointing at Closient. |
Any host that implements the GS1 DL resolver protocol will serve the
same identifier. The Closient hosted pages live under
id.closient.com and *.gtin.one; the Brand.preferred_resolver_host
column stores which one to emit for that brand’s products.
Full example
Inputs:
- GTIN-14:
09506000134352
- Lot:
ABC123
- Expiry:
2030-09-01 (= 300901 YYMMDD)
- Serial:
SN9999
- Best-before:
2027-12-31 (= 271231 YYMMDD, AI 15 → query string)
- Link type: PIP (product information page)
- Host:
https://id.closient.com
Canonical URL:
https://id.closient.com/01/09506000134352/10/ABC123/17/300901/21/SN9999?15=271231&linkType=gs1:pip
In Python (Closient internal helper):
from apps.resolver.digital_link_builder import build_digital_link
url = build_digital_link(
host="https://id.closient.com",
gtin="09506000134352",
lot="ABC123",
expiry="2030-09-01",
serial="SN9999",
extra_query={"15": "271231"},
link_type="gs1:pip",
)
That helper takes care of the AI ordering, percent-encoding, and date
formatting in one call.
Common mistakes
- Wrong qualifier order. Emitting
/01/<gtin>/21/<serial>/10/<lot>
is parseable but not canonical; the resolver redirects to the canonical
ordering and analytics will conflate the two URLs.
- Leaving out the qualifier on a serialized item. A GTIN+serial URL
(
/01/<gtin>/21/<serial>) resolves to that specific item; a GTIN-only
URL (/01/<gtin>) resolves to the class. Emit the qualifier when you
have the data — class-level resolution can’t drive item-level
experiences like recalls.
- Encoding the AI itself. AI digits are path literals and must NOT
be percent-encoded — only the value slot is.
- Wrong link-type CURIE.
linkType=gs1:pip (correct CURIE) versus
linkType=pip (bare token, ambiguous) — always emit the namespaced
form. Closient’s test suite normalizes both, but partner resolvers may
not.
- Hardcoding
id.gs1.org for brand-owned products. Closient brands
have their own preferred resolver host; use that so the brand owns the
surface and gets the analytics.
- Not URL-encoding non-ASCII or reserved characters in lot/serial.
A lot of
LOT-2024/Q1 becomes LOT-2024%2FQ1.
What Closient does with the URL
Once emitted, the URL can be:
- Encoded as a QR code — Closient’s QR rendering service
(
apps/products/services/qr_url.py) takes any Digital Link URL and
produces a print-ready QR. See the Print Digital Link MCP tool or
POST /products/api/v1/qr/render.
- Hosted by Closient’s resolver — if the URL points at
id.closient.com or a *.gtin.one brand domain, Closient serves
the destination (hosted page, redirect, JSON linkset, branded video,
etc.) based on linkType and the brand’s resolver configuration.
- Verified for serialized items — call
GET /resolver/api/v1/verify/01/{gtin}/21/{serial} to confirm an
item is authentic + active.
decode-gs1-ai — inverse: take a Digital Link URL apart into AIs.
resolve-gtin — once you have the URL, fetch product detail.
capture-epcis-event — emit a supply-chain event keyed on the same
GTIN + serial.
closient-api-quickstart — authenticate before calling
/products/api/v1/qr/render or /resolver/api/v1/verify/....
Authoritative references
- GS1 Digital Link Standard 1.4 — sections 4 (URI syntax), 5
(canonicalization), 6 (link types).
- ISO/IEC 18975:2024 — formalization of GS1 Digital Link.
- GS1 link-type vocabulary:
https://www.gs1.org/voc/.
- Closient internal:
apps/resolver/digital_link_builder.py,
apps/resolver/gs1_link_types.py, apps/products/services/qr_url.py.