How to Test a GS1 Digital Link Resolver for Conformance
A GS1 Digital Link resolver either behaves the way the standard says, or it doesn't. The gap between "our resolver works" and "our resolver is conformant" is where most self-built implementations quietly sit, and you usually find out when a retailer's system does something your redirect didn't expect.
This is a practical checklist. Every example below is a real request you can run against a live conformant resolver right now, so you can compare the output to your own.
Why conformance matters more than it sounds
A resolver that mostly works is fine until it isn't. The failure mode is specific: your QR codes are already printed, already on shelves, already in warehouses. They all point at one domain. If that domain answers requests incorrectly, you cannot fix it by reprinting.
The parties that care are not consumers. They're retailer systems, supply chain software, and standards bodies, all of which follow the specification rather than guessing. Consumers scanning with a phone camera get a redirect and never notice the difference. Automated systems notice immediately.
What a conformant resolver has to get right
1. The /.well-known/gs1resolver descriptor
Every conformant resolver publishes a machine-readable description of itself at a fixed path. This is how other systems discover what your resolver supports without trial and error.
curl https://id.sunriseqr.com/.well-known/gs1resolver
The response is JSON describing the resolver:
{
"name": "SunriseQR Resolver",
"supportedPrimaryKeys": ["01"],
"supportedLinkType": [
{ "namespace": "https://gs1.org/voc/", "prefix": "gs1:" }
],
"linkTypeDefaultCanBeLinkset": false,
"jsonLdContextLocation": "https://ref.gs1.org/standards/resolver/linkset-context",
"contact": { "fn": "SunriseQR" },
"resolverRoot": "https://id.sunriseqr.com"
}
Every field there comes from GS1's published description file schema, and nothing else does. That constraint is the actual test, and it is stricter than it looks.
The schema permits additional properties, so a descriptor full of invented fields is still technically valid. It is also read by nobody. Our own descriptor used to carry seven of them, including operatorName, conformanceLevel, supportsLinkset and supportsLanguageNegotiation. They looked informative and communicated nothing, because no client is written to look for them. If a capability is worth declaring, it belongs in a schema field or it does not belong in the file.
Two details worth checking in your own:
supportedLinkTypeis a CURIE prefix map, not a list of link types. It declares which namespace abbreviations a client may use, sogs1:pipcan be expanded tohttps://gs1.org/voc/pip. There is deliberately no schema field for enumerating link types, because which links exist is a property of each product, discoverable from that product's linkset, not of the resolver. A field namedsupportedLinkTypes, one character away, means something else entirely and is not in the schema.- Omitting a field is a statement too. We leave out
supportedContextValuesEnumerated,supportedContextValuesExternalandextensionProfilebecause we implement none of them, and emitting them would assert something untrue. Empty or filler values are worse than absence.
Test yours: the endpoint must return HTTP 200 with Content-Type: application/json, and every key in it should appear in the schema. If it 404s, nothing downstream can discover your capabilities. If it is full of fields the schema has never heard of, nothing downstream reads them.
2. URI parsing across all four layers
A Digital Link URI is not a flat string. It has structure, and a conformant parser has to handle each layer:
/01/07501234567893/10/BATCH-A1?17=261231
| | | | |
AI Primary Qualifier Data Attribute
Value (batch) (expiry date)
<-- Layer 2 --> <Layer 3> <-- Layer 4 -->
01 is the Application Identifier for GTIN. 10 is a batch or lot qualifier. 17 is an expiry date passed as a query parameter. A resolver that only handles /01/{gtin} and chokes on qualifiers will fail the moment a traceability use case shows up.
3. GTIN validation, including the check digit
GTINs must be 8, 12, 13, or 14 digits with a valid check digit. This is where the most important distinction in resolver behavior lives.
A malformed GTIN is a client error (400):
curl https://id.sunriseqr.com/01/12345678901234
{
"error": "Invalid GTIN",
"detail": "Check digit failed for \"12345678901234\" — expected 1, got 4"
}
A well-formed GTIN that isn't in the catalog is a 404:
curl https://id.sunriseqr.com/01/00000000000000
{
"error": "GTIN not found",
"detail": "No active links configured for 00000000000000"
}
Test yours: send both. If your resolver returns the same status for a mangled number and an unknown-but-valid product, it's collapsing two genuinely different conditions. Automated clients use that distinction to decide whether to retry, report a data error, or move on.
4. Unsupported identifiers must fail clearly
Not every resolver supports every Application Identifier. That's allowed. What isn't allowed is failing confusingly.
curl https://id.sunriseqr.com/414/1234567890123
{
"error": "Unsupported Application Identifier",
"detail": "AI \"414\" is recognized but not yet supported by this resolver"
}
Test yours: a recognized-but-unsupported AI should say so, not return a generic 500 or a redirect to your homepage. The descriptor's supportedPrimaryKeys should agree with what the resolver actually does.
5. Content negotiation
The same URI has to return different things depending on what the client asks for. This is the requirement most homegrown resolvers skip, because a phone camera never exercises it.
| Request | Expected response |
|---|---|
| No special headers | HTTP redirect to the default link |
Accept: application/linkset+json | Full linkset JSON, not a redirect |
?linkType=all | Full linkset JSON |
?linkType=gs1:pip | Redirect to the product information page link |
Test yours: request the same GTIN four ways. A resolver that redirects regardless of the Accept header is not conformant, however well it works in a phone camera.
6. The Link header
Even on a plain redirect, the response should carry a Link header advertising the other available link types. This lets a client discover alternatives without a second request.
curl -I https://id.sunriseqr.com/01/00049000028904
link: <https://…#shop>; rel="https://gs1.org/voc/pip"; type="text/html"; title="Product Information Page",
<https://…#shop>; rel="https://gs1.org/voc/hasRetailers"; type="text/html"; title="Buy here",
<https://id.sunriseqr.com/01/00049000028904?linkType=linkset>; rel="linkset"; type="application/linkset+json"
Note the third entry. Alongside the product's own links, the header points back at the full linkset, so a client that wants everything knows exactly where to ask without guessing at content negotiation. Each entry also carries title and type, which is what lets an automated consumer choose between links rather than just following the first.
Test yours: curl -I a plain resolution and check whether Link is present, lists more than the destination you were sent to, and advertises the linkset.
7. Language negotiation
Where multiple links of the same type exist in different languages, the resolver should honour Accept-Language and pick the best match rather than always serving the first row it finds.
The quickest self-assessment
Run these against your own resolver and compare:
GET /.well-known/gs1resolverreturns 200 JSON, and every key in it is in the schema- A bad check digit returns 400, not 404 or 200
- An unknown but valid GTIN returns 404, not 400 or 200
- An unsupported AI returns a clear 400, not a 500
Accept: application/linkset+jsonreturns JSON, not a redirect?linkType=allreturns the full linkset- A plain redirect carries a
Linkheader Accept-Languagechanges which link is chosen
If any of those surprise you, that's the list to work through.
Why this is an argument for buying rather than building
None of the above is difficult in isolation. The difficulty is that it's a permanent commitment.
GS1 publishes spec updates. Each one means re-reading the standard, adjusting behavior, and re-running conformance testing. Meanwhile the resolver has to stay up, because every printed code depends on it, and there is no rollback that involves reprinting packaging.
That's the real calculation in build versus buy: not whether your team can implement content negotiation, but whether you want to own conformance maintenance and uptime for as long as your codes are in circulation.
SunriseQR's resolver is validated against the official GS1 conformance test suite, the same one retailers and standards bodies use, and we track spec changes so the codes you printed last year keep behaving correctly this year.