One code on the pack. Every answer behind it.
This is the working handbook: what to set up, in what order, and exactly how the resolver behaves when a pack is scanned. The conformance narrative · what we implement, what we do not, and the standard each claim traces to · lives on the GS1 overview. The resolver is conformant to the GS1-Conformant Resolver Standard 1.2.0, verified with GS1's official test suite, self-asserted · GS1 does not certify resolvers, ours or anyone's.
The model
A GS1 Digital Link puts a plain web URI on the pack instead of a bare barcode. The identifier your products already carry (a GTIN, most commonly) becomes the path of a URL on your domain, and scanning it reaches whatever you have registered for it · a product page, a recall notice, instructions, a per-unit story.
Five things exist in the system, and everything on this page is one of them:
The catalogue is a product master-data registry your workspace owns and enters or imports itself · nothing is fetched from a GS1-hosted database for display. Manage it in Dashboard → GS1, over the REST API, or through MCP.
Primary keys
All 16 primary identification keys the Digital Link syntax defines are supported, each with every qualifier the standard gives it · the standard permits supporting a subset of keys but not a subset of a key. Every value is validated before anything is stored or printed: check digits, issued lengths (a GTIN is only ever 8, 12, 13 or 14 digits), and qualifier grammar.
| AI | Key | Name | Qualifier AIs | Check |
|---|---|---|---|---|
| 00 | SSCC | Serial Shipping Container Code | · | mod-10 |
| 01 | GTIN | Global Trade Item Number | 22, 10, 21 or 235 (exclusive) | mod-10 |
| 253 | GDTI | Global Document Type Identifier | · | mod-10 |
| 255 | GCN | Global Coupon Number | · | mod-10 |
| 401 | GINC | Global Identification Number for Consignment | · | none |
| 402 | GSIN | Global Shipment Identification Number | · | mod-10 |
| 414 | GLN | Global Location Number (physical location) | 254 or 7040 (exclusive) | mod-10 |
| 415 | GLN (pay to) | GLN of the invoicing party | 8020(required) | mod-10 |
| 417 | Party GLN | Party Global Location Number | 7040 (exclusive) | mod-10 |
| 8003 | GRAI | Global Returnable Asset Identifier | · | mod-10 |
| 8004 | GIAI | Global Individual Asset Identifier | 7040 (exclusive) | none |
| 8006 | ITIP | Individual Trade Item Piece | 22, 10, 21 | mod-10 |
| 8010 | CPID | Component / Part Identifier | 8011 | none |
| 8013 | GMN | Global Model Number | · | char pair |
| 8017 | GSRN-P | Global Service Relation Number (provider) | 8019 | mod-10 |
| 8018 | GSRN | Global Service Relation Number (recipient) | 8019 | mod-10 |
- An exclusive qualifier forms a distinct identifier with its key (GTIN + 235 is a UPUI, GLN + 7040 is an FID) and never combines with the key's other qualifiers.
- AI 415's qualifier is mandatory: a bare
/415/{gln}is not a legal Digital Link URI and is answered with 400, per URI Syntax 1.6 §8.5. - GTIN-8/12/13 are accepted and stored canonically at 14 digits, and the length you issued at is remembered · it decides where your company prefix sits inside the number.
- Any of the GS1 Syntax Dictionary's application identifiers can be attached to an item as master data; each is validated against its published format and encoded into the printed URI's query string.
Licences: who may publish a key
A Digital Link asserts that the identifier inside it was licensed to the publisher by GS1. No resolver standard checks that, so this platform does: you register the GS1 Company Prefix you licensed in Dashboard → GS1 → Licences, and every publishing action, on every origin · the shared host and your own custom domain alike · checks the key against your prefixes first. Owning a domain is not permission to print someone else's GTIN on it.
- Attested vs verified. A licence is attested: your declaration, stored with the user, IP and timestamp · which is what we would produce in a dispute. With a GS1 registry connection configured it can be verified against the registry instead. The status is always shown honestly next to every item.
- Restricted ranges are refused outright: store-internal prefixes (02x/04x/2xx), coupons, refund receipts and Bookland ISBNs identify nothing outside one retailer and are the most common self-serve mistake.
- Prefixes are checked for overlap, not equality. Two workspaces cannot hold prefixes where one would swallow the other's key space.
- On the shared host (https://tap01.link) a key also needs a claim: exactly one workspace can own a given identifier there, first come first served among licensed workspaces. On your own custom domain the hostname itself is the tenant signal and no claim is needed · that is the production path for GS1 anyway.
- A draft claims nothing. Claiming on create would let anyone lock a competitor out of an identifier without ever publishing.
- Declaring a prefix claims your existing items. Active items you created before the licence are claimed on the shared host the moment the prefix is added · no re-create, no second charge. Items another workspace already claimed stay theirs.
- Adding a licence is a legal declaration and is dashboard-only: it is deliberately not exposed to API keys or MCP agents, because it must record a real human user.
The linkset: what a scan finds
Each item carries a linkset: destinations typed with the GS1 Web vocabulary (57 selectable link types · gs1:activityIdeas, gs1:allergenInfo, gs1:appDownload, gs1:backgroundInfo, gs1:careersInfo, gs1:certificationInfo, …) and scoped to a qualifier level. A recall notice can be pinned to one lot; a warranty page to one serial; the product page to the key itself.
When a scan carries a qualifier no link is registered for, resolution walks up · never into a dead end:
- Every item always has a key-level default link · a linkset without one is rejected at write time, so a plain scan always has somewhere to go.
- Language and format negotiation run on every scan: register the same link type with
different
hreflangvalues and a French shopper's phone gets the French page, with no app involved. - Editing the linkset is free, forever. Re-pointing a printed code is the whole product.
- Machine access: any client asking with
Accept: application/linkset+json(or?linkType=linkset) gets the linkset as RFC 9264 JSON instead of a redirect.
The carrier: a symbol that survives printing
The carrier generator produces the QR symbol for an item (or any qualifier scope of it) to GS1's print guidance, and reports physical facts · because the artwork goes to a press, not a screen.
- No logo overlay, ever. The GS1 carrier generator has no image path at all: a logo spends the error-correction budget a symbol needs to survive a conveyor and a scuffed pack. (The ordinary QR studio is a separate path and allows logos.)
- The QR is a plain QR Code (]Q1), not a GS1 QR Code (]Q3) · a Digital Link URI is a URI. The FNC1/element-string symbology is a different job, and it is the one the linear symbol does: a GS1-128 from here announces ]C1 and carries the element string.
- The print sheet sizes each symbol in millimetres, carries the human-readable key under the symbol, and says "print at 100%, do not scale or crop" · the same sheet prints the same physical code on A4 and Letter.
- Compression (shorter URIs, denser symbols) is available and every compressed URI is decompressed again and compared with its input before it is returned; if the round trip does not hold you get the uncompressed URI · longer, and correct. A few keys (GMN, GRAI, and the AI 235/7040/417 forms) cannot be compressed by GS1's own toolkit and always fall back the same way.
Serials: per-unit codes without a database of units
A serialised run gives every unit its own scannable identity (AI 21). Serials are derived, not stored: no row exists per serial, so a batch of 100,000 costs no storage, a re-export reproduces the identical codes, and a lost CSV never means re-buying a print run.
- The HMAC is what makes a serial unguessable, not just reproducible: a counterfeiter who collects real serials still cannot mint one that verifies, and verification compares in constant time.
- The job id is the batch. Keep the
X-Job-Idheader from an issuance: re-deriving that batch later is free and yields byte-identical serials. GET /api/v1/gs1/verify?serial=…answers "did we issue this?" by recomputation. What it deliberately is not: an EPCIS event trail · we can say a serial was issued, not where it has been.- Caps: 100,000 serials per CSV call on the API, 10,000 through MCP, 500 on a server-rendered print sheet. CSV import of catalogue items caps at 5,000 rows per call.
Asking whether a serial in the wild is one of yours is a recomputation, not a lookup · which is why the answer never depends on a table we have to keep growing:
curl "https://taproute.io/api/v1/gs1/verify?serial=7F2A00001AK3VP7QZM" \
-H "Authorization: Bearer tr_..."
# { "issued": true,
# "jobId": "7f2a1b3c-9d4e-4f5a-8b6c-1d2e3f4a5b6c",
# "keyAi": "01", "keyValue": "09506000134352",
# "index": 42, "issuedAt": "2026-07-16T09:12:44.000Z" }
# A serial we never issued, or one edited by hand, fails the HMAC and answers:
# { "issued": false, "reason": "no batch of this workspace issued that serial" }The resolver: what a scan gets back
Every origin · the shared host and each custom domain · self-describes at /.well-known/gs1resolver and answers per the Resolver Standard:
| 302 | The normal scan: redirect to the negotiated destination, with the request's query passed through and a Link header pointing at the machine-readable linkset. |
| 300 | Several links are equally good: a choice page rather than a coin flip. |
| 400 | A syntactically invalid URI: bad check digit, wrong-key qualifier, non-issued GTIN length. Malformed is an error, never a silent 404. |
| 404 | A valid identifier nothing is registered for · and a requested linkType that is not registered (the 1.2.0 rule: never fall back to the default). |
| JSON | ?linkType=linkset or Accept: application/linkset+json returns the linkset as RFC 9264 JSON; ?linkType=all from a browser renders it as HTML with JSON-LD embedded. |
- Content negotiation (Accept-Language / Accept) applies to the plain redirect too · a
phone camera sends no
linkType, and the multilingual story has to work for an actual shopper, not just API clients. - Fully and partly compressed Digital Link URIs are decompressed and resolved.
- Only an active item resolves. Draft and archived items are actively removed from the edge, so status genuinely controls world-visibility.
- GS1 scans are metered like every other scan (1 credit) and appear in the same analytics.
Product pages: what the code lands on
A linkset entry can point at a URL you own, or it can be bound to a page we host. Binding is not the same as pasting our URL into the box, and the difference is worth understanding before you print anything.
| 1 credit | A bound page is rendered by the resolver itself, so a scan is one scan. Pasting a URL to one of our own pages instead costs two: the resolver bills the redirect and the page bills the arrival. |
| Your domain | The page is served on whatever host the code was printed against. A pasted URL is written against one origin, so a code on your own domain would bounce the scanner onto ours. |
| Batch and serial | The page can read the lot and serial from the code that was scanned. A redirect cannot: they are path segments, and a 302 carries only the query string. This is what lets one page answer for a specific batch without a page per unit. |
| Per link type | Applying a product template registers one entry per section, all pointing at the same
page, so ?linkType=gs1:allergenInfo opens it on the allergens and a plain
camera scan opens it at the top. |
Fourteen templates ship for food, drink, wine, apparel, footwear, cosmetics, electronics, appliances, batteries, toys, detergents, over-the-counter healthcare and authenticity checks. Each one is example content you replace, not a form you fill in · see them rendered.
A product page is not a Digital Product Passport. A passport is read by machines, carries
per-value translations, partitions its content by who may read it and keeps an archive of what
it claimed. These pages do none of those four, and gs1:dpp stays pointed wherever
you host yours.
What it costs
The two prices are split deliberately: the item carries the price the incumbents meter on,
the serial stays at the volume rate. An updated CSV import row and a jobId re-derive are free · both were paid for at creation, and
charging again would bill you 200 credits to fix a typo. Every charge is
taken before the write and refunded if the write fails; a short wallet gets 402, never work-now-invoice-later.
Conformance, and the gaps
GS1 does not certify resolvers. No badge, no registry, no accreditation, which means every "GS1 certified" claim in this category is decorative · ours would be too. What is real is that the test suite is GS1's own and public, so you can run it against us yourself. The dated run is on the GS1 page and the full compliance document is a file you can hand to a compliance team.
Both columns below ship on the same visual plane, deliberately. The right-hand column is not a disclaimer to be tidied away later: it is the reason the left-hand one is believable.
Implemented, and test-verified
- ✓/.well-known/gs1resolver
- ✓RFC 9264 linkset, application/linkset+json
- ✓?linkType=linkset, and JSON-LD in the HTML
- ✓gs1:defaultLink enforced on every write
- ✓Qualifier hierarchy walks up: serial, then lot, then key
- ✓404 on an unregistered linkType, the 1.2.0 change
- ✓300 on ambiguity, 400 on invalid syntax
- ✓Content negotiation on Accept and Accept-Language
- ✓Compressed URIs expanded at the edge
- ✓Mod-10 and GMN check-character-pair validation
- ✓GTIN-8, 12 and 13 canonicalised to 14, issued length preserved
- ✓Linear symbology per key: EAN-13, UPC-A, EAN-8, ITF-14 or GS1-128
- ✓Dual marking on one artwork, centres inside §4.1.2’s 50 mm
Not implemented
- The QR is a plain QR symbol, not a GS1 QR Code: conformant Digital Link payload, but its symbology header is ]Q1, not ]Q3. The linear symbol is the GS1-mode one.
- No compression for AIs 235, 7040, 417, GMN and GRAI. A gap in the upstream GS1 toolkit; we fall back to correct uncompressed URIs.
- No Data Matrix, which is what healthcare largely uses. We emit QR, GS1-128, EAN-13, UPC-A, EAN-8 and ITF-14.
- No ISO 15415 print grading. You get the geometry and the print facts, not a graded score for a printed sample.
- No EPCIS event trails. Verify answers "was this issued", not "where has it been".
- Context-based link selection is not implemented. ?context= is consumed per the standard but does not influence which link is chosen.
- No certification exists to claim. Conformance is self-declared via the description file plus GS1’s public test suite, which we pass · the same is true of every resolver you have ever read about.
The three defects below are in GS1's OWN reference implementation, which is Apache-2.0 and public · you can go and read the lines. Every compressed URI we produce is decompressed again and compared to its input before it is printed, because a wrong identifier on a pack cannot be recalled.
- parseInt on a 17- or 18-digit key. Every SSCC, GSIN, ITIP and GSRN compressed to a different identifier, silently, because the value passed through a float above 2^53. Moved to BigInt.
- The GRAI check digit verified at position 13, against the toolkit’s own N14 format and its own 14-digit regex. It threw on valid GRAIs. Patched to 14, pinned in both directions.
- Character-set-82 fields narrowed to digits only, which silently blocked compression of any lot or serial containing : ; < = > ?
Start to finish
- Declare your GS1 Company Prefix in GS1 → Licences. One attestation covers every key under the prefix, and a licence added later lights up every existing item it covers.
- Create the item (200 credits, once) with its key, name and default URL · in the dashboard, by API, by CSV import, or by an MCP agent. Keep it a draft until it is real; drafts resolve nowhere and claim nothing.
- Build the linkset: typed destinations per qualifier scope,
languages via
hreflang. Free, editable forever. - Print the carrier from the item's QR tab or
carrier.svg: pick the domain it resolves on, check the millimetre size it reports, hand the SVG to the press at 100%. - Serialise if units matter (1 credit per serial): a CSV stream of per-unit URIs plus element strings. Keep the job id · the batch re-derives free.
- Watch it live: scans land in analytics and the live feed like every other code, and
/api/v1/gs1/verifyanswers authenticity questions from the field.