We operate a GS1-conformant Digital Link resolver
Splide runs a GS1 Digital Link resolver at id.splide.com, conforming to the GS1-Conformant Resolver Standard, version 1.2.1.
It passes GS1's own published conformance test suite. You do not have to take that from us. The suite is hosted by GS1 at ref.gs1.org/test-suites/resolver/, it runs in your browser against any public endpoint, and it will tell you the same thing about ours that it told us.
A note on wording, because precision matters to this audience: GS1 operates no conformance certification, accreditation or badge for resolvers. Nobody can issue one. We therefore do not describe ourselves as GS1-certified or GS1-accredited, and neither should anyone else. A passing run of GS1's own suite against a live public endpoint is the closest thing that exists, and it is checkable by anyone, which is exactly what makes it worth having.
Check it yourself
Every line below is a live request against production. No sandbox, no credentials.
# 307 to the default link curl -I https://id.splide.com/01/09520123456788
# a specific link type curl -I "https://id.splide.com/01/09520123456788?linkType=gs1:recipeInfo"
# RFC 9264 linkset curl -H 'Accept: application/linkset+json'
"https://id.splide.com/01/09520123456788?linkType=all"
# resolver description file curl https://id.splide.com/.well-known/gs1resolver
Behaviour, as verified:
| Request | Response |
|---|---|
| Known GTIN, no linkType | 307 to the default link |
| linkType with a match | 307 to that link |
| linkType with no match | 404, never a silent fall back to the default |
| Unknown GTIN | 404 |
| Invalid check digit | 400 |
| Qualifiers out of the fixed order | 400 |
| GTIN-13 given unpadded | 307, normalised to 14 digits |
| linkType=all | 200, application/linkset+json |
| /.well-known/gs1resolver | 200 |
| HEAD | 307, same headers, no body |
| OPTIONS | 204, Allow: GET, HEAD, OPTIONS |
| POST | 405 |
CORS is open (Access-Control-Allow-Origin: *) with Link, Location and Content-Type exposed, so a browser-based client can actually read what it needs. Content negotiation varies on Accept and Accept-Language.
The two failure modes we consider most important are the ones that are easiest to get wrong and hardest to notice: a linkType that matches nothing returns 404 rather than quietly falling back to the default link, and the qualifier order /01/{gtin}[/22/{cpv}][/10/{lot}][/21/{serial}] is fixed, so anything else is malformed and gets a 400 rather than a best guess.
What the URI structure actually addresses
The part that gets undersold, including by us until recently. A Digital Link URI is not a redirect with extra steps. It is an addressing scheme with four independent axes, and we implement all four.
Identifier type. GTIN is one of fifteen primary keys we answer on:
00 SSCC logistics units, cases and pallets 01 GTIN trade items 253 GDTI documents 255 GCN coupons 401 GINC / 402 GSIN consignments and shipments 414 GLN / 417 PARTY physical locations and parties 8003 / 8004 GRAI / GIAI returnable and individual assets 8006 ITIP components and pieces of a trade item 8010 CPID components and parts 8013 GMN product models 8017 / 8018 GSRN service relationships
So the addressable estate is not just retail packs. It is cases, pallets, locations, assets, equipment, models, parts and service relations.
Granularity. Within a GTIN, the qualifier chain narrows the subject:
| URI | Identifies |
|---|---|
| /01/{gtin} | the product |
| /01/{gtin}/22/{cpv} | a consumer product variant |
| /01/{gtin}/10/{lot} | a specific batch or lot |
| /01/{gtin}/10/{lot}/21/{serial} | one individual unit |
The order is fixed and enforced. Anything else is malformed and returns 400 rather than a best guess.
Resolution walks up the chain. A request for a serial we hold no entry for falls back to its batch, and then to the product, rather than 404ing. Verified live: /01/09520123456801/10/AB2143/21/0001 resolves today even though no serial-level entry exists for it. That is what lets a customer start at product level and add batch or unit depth later, against packaging that is already printed and already in the field.
Link type. The request names which kind of answer it wants, from GS1's published vocabulary, so a machine can ask a precise question. We currently serve pip, recipeInfo, nutritionalInfo, allergenInfo, sustainabilityInfo and the default link on the demonstration data, and the vocabulary itself is open, covering instructions, safety data sheets, certification, traceability, warranty, recall status, repair and end-of-life information.
Request context. Accept-Language and Accept both participate, so the same URI returns a different destination for a different language, and a different representation for a machine than for a browser.
The four axes are independent, which is the point. One printed code, and the answer is a function of who asked, what they asked for, when, and how precisely they named the subject.
The linkset is an integration surface, not a formality
?linkType=all with Accept: application/linkset+json returns every link the subject has, as RFC 9264, anchored on the canonical URI.
This is the part worth taking seriously commercially. It means a retailer, marketplace, distributor or regulator can ingest a product's full link set from the product identifier alone, with no per-partner feed, no portal upload and no credential exchange. The brand maintains one set of answers. Everybody else dereferences the same address and gets the current state, because nothing is cacheable.
For Digital Product Passport work under the EU's ESPR this is the mechanism rather than a nice-to-have: the passport has to be reachable from the item, and this is the carrier the regulation's data carrier requirements point at.
Responses are never cacheable, and that is enforced
The resolver emits no Cache-Control, Expires, ETag, Age or Pragma header on any response.
This is not a preference. A cached redirect makes a batch recall a lie. If an intermediary is still handing out yesterday's answer, the switch that is supposed to make every unit of a bad batch show a safety notice does nothing, and nobody finds out until it matters.
It is worth being explicit about why this needs guarding rather than assuming. The resolver runs as a Cloudflare Worker on a custom domain, which means it runs ahead of the zone cache and is itself the origin. An edge cache-bypass rule sitting in front of it would be inert. So the invariant is enforced by tests in our own suite instead, on the reasoning that this is precisely the property a later performance refactor removes without noticing.
What is running
- Cloudflare Workers with D1 for link data, so there is no origin server, no home connection and no single machine in the path.
- Multi-tenant from the first line of code. The service reads the request Host header and answers from that customer's link set, which is what allows a brand's codes to carry their own domain while we operate what answers. A GTIN registered under one host returns 404 on another.
- A previous deployment, on a hardened Debian host behind a Cloudflare Tunnel, is still running as a rollback target. Reverting is one DNS record.
- Written from scratch rather than adopted. We evaluated GS1's community reference implementation first and rejected it: it disclaims conformance, is not maintained by GS1 and has community-only support. Operating the service is the point, so the code had to be something we could actually answer for.
Latency, since somebody always asks: roughly 210ms from Pretoria. D1 has no African region, so the database sits in Western Europe and that is the floor. This was a deliberate trade of about 175ms against removing every single-point-of-failure from the path, and it is the right way round for something that has to answer for years.
Where the demo is
demo.splide.com carries a demonstration brand on the GS1 952 prefix, which is reserved for exactly this purpose.
/01/09520123456788resolves to a product page.- The same code with
linkType=gs1:recipeInforeaches a brewing guide, and there are nutritional, allergen and sustainability link types alongside it. Accept-Language: afreturns a genuine Afrikaans page, translated, not the English page wearing a language tag.- Batch AB2143 of a second product carries a pre-written recall notice that can be switched on and off. When it is on, that batch answers with the notice while the product page and every other batch are untouched. No restart, no redeploy, and the change is live in about a second.
What we are looking for
We are a software and operations company, not a printer and not a data consultancy, and we think that slot is empty in this market. Our interest is in operating the answering layer for brands whose packaging carries these codes, and in working alongside the printers and data specialists who already serve them rather than competing with either.