# GET /01/{gtin}/21/{serial}

Resolve a GS1 Digital Link (AI 01 + AI 21) to the item's public page. Public endpoint, no API key, which answers with a redirect.

Source: https://docs.sealtrust.io/en/reference/get-gs1-gtin-serial/

---

You turn a GS1 Digital Link into the address of the item's public page. The
response carries the code 302 and the destination address in the `Location`
header. The response body is empty.

## Authorization

None, public endpoint. It expects neither an API key, nor a session cookie, nor
an `Authorization` header. A server-to-server call is accepted.

Call this endpoint on `https://api.sealtrust.io`. The same route also answers
under the `/v1` prefix, at
`https://api.sealtrust.io/v1/01/{gtin}/21/{serial}`. Both addresses call the
same code. For a new server integration, use the `/v1` form. The form without a
prefix exists because it is the `/01/…/21/…` path structure that makes an
address a GS1 Digital Link, so that is the one a code scanner encounters.

The domain we write into the GS1 links we produce is configurable, and defaults
to `https://id.gs1.org`. The identifier we declare to the European digital
product passport registry is the short address `/p/{serial}`, because a full GS1
link exceeds the registry's 50-character limit. The QR code we print on an item
also encodes `/p/{serial}`.

> [!ATTENTION] Call this endpoint from a server
> A script launched from a web page gets nothing usable here. Our origin policy
> authorizes only a closed list of sites, and we expose to scripts neither the
> `Location` header nor the `X-RateLimit-*` headers. Scanning a QR code or
> clicking the link opens the page normally: this policy concerns only calls
> launched by a script from another page.

> [!INFO] The two codes the address carries
> `AI 01` is the GTIN, the 14-digit international article number that designates
> a commercial reference. `AI 21` is the serial number, which designates one
> precise unit of that reference. The pair of the two designates a unique
> physical object.
>
> `{serial}` is the 12-character public serial number, the same one carried by
> the short address `/p/{serial}`. It is not the token identifier on the chain.
> Both forms designate the same unit and lead to the same page.

A GS1 link may also carry only the GTIN, without `AI 21`. It then designates the
commercial reference and resolves to the model's passport, shared by all the
units that carry that same product code. See `GET /01/{gtin}`. The ESPR
regulation allows a passport at the model, batch or unit level. We serve the
model level and the unit level. Nothing requires you to serialize every unit.

## Rate limit

This path has no limit of its own. It falls under the general API counter,
counted per calling IP address over a 60-second window. That counter is shared
by all the routes that have no limit of their own, and it is shared by all
values of GTIN and serial number. Browsing a thousand different links consumes a
thousand calls from the same budget.

Every response carries the headers `X-RateLimit-Limit`, `X-RateLimit-Remaining`
and `X-RateLimit-Reset`, the last one giving the reset time in seconds since
January 1, 1970.

This endpoint consumes no quota from your plan.

> [!ATTENTION] Plan for the 429 code in your client
> `X-RateLimit-Remaining` drops to 0 as soon as you exceed the general counter.
> Slow down before you get there. A call beyond the budget may receive a 429
> code accompanied by a `Retry-After` header in seconds: wait that long, then
> try again. The value of the general counter may change without this path being
> modified, so set your client on the headers of your responses and hard-code no
> number in your code.

## Path and query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `gtin` | `string` | yes | The GTIN of the commercial reference. We strip every character that is not a digit, including dashes and spaces, then we pad on the left with zeros up to 14 digits. A GTIN with no digit at all, or with more than 14 digits, gives 400. We also check its check digit: see below. |
| `serial` | `string` | yes | The unit's public serial number: 12 characters from the Crockford Base32 alphabet, which excludes the letters I, L, O and U. We strip edge spaces, and case does not matter. Any other length, or any character outside that alphabet, gives 404. |
| `linkType` | `string` | no | Asks for the passport rather than the product page. Five values trigger it, listed below. Case and edge spaces do not matter. |

We map the letters I and L to the digit 1, and the letter O to the digit 0,
before looking for the unit. So you get the right answer even for a number
retyped by hand with a lookalike character.

We ignore every other query parameter. An address that drags along the
parameters of an NFC scan, or the campaign markers of a shared link, resolves
exactly like the bare address.

### The five values of `linkType`

| Value | Effect |
| --- | --- |
| `dpp` | Redirects to the unit's passport. |
| `passport` | Same. |
| `gs1:dpp` | Same. |
| `gs1:digitalproductpassport` | Same. |
| `digitalproductpassport` | Same. |

Any other value gives the same response as a missing parameter, so the redirect
goes to the product page. You can write the parameter name `linkType` or
`linktype`.

### The GTIN check digit

The last digit of a GTIN is its check digit: the GS1 modulo 10 rule computes it
from the digits that precede it. We recompute it and compare before any lookup.

So count 8, 12, 13 or 14 digits, the four lengths a GTIN can have, and check
that the last one really is the check digit of the preceding ones. A GTIN that
falls outside that rule receives a 400 code, with the body
`{"detail": "Invalid GTIN: the check digit does not match."}`.

Copy the GTIN from the reference's barcode. A typo on a single digit is then
visible as soon as you make the call.

### What the GTIN has to satisfy

We compare the path's GTIN to the GTIN of the unit's model. The two must be
identical. You draw two direct consequences from this.

A unit whose model carries no GTIN is not addressable in this form. It remains
addressable by its short address `/p/{serial}`.

A real serial number paired with a GTIN that is not its own answers 404, with
exactly the same body as an unknown serial number. You cannot tell the two cases
apart, and that is deliberate: the response never confirms that a serial number
exists.

### A unit withdrawn from the catalog keeps answering

A unit replaced by a later mint and an archived unit remain resolvable. We
redirect them to their passport rather than to the product page, because the
product page no longer displays them. A code printed on an object that is still
in someone's hands must not answer "unknown".

A serial number designates a single unit. When that unit has been replaced by a
later mint, it is the current version that answers.

## Request body

None. This is a `GET` request, everything goes through the path.

## Example request

The three examples resolve the same link and do not follow the redirect, so that
you can see the `Location` header. They call the `/v1` form, the one we
recommend for a server integration. Remove `/v1` to call the address as it
appears in a GS1 link: the response is identical.

The TypeScript example runs server-side, under Node 18 or later.

:::onglets
```bash title="curl"
curl -i https://api.sealtrust.io/v1/01/03701234567891/21/0000000000AB
```
```typescript
const gtin = "03701234567891";
const serial = "0000000000AB";

const response = await fetch(
  `https://api.sealtrust.io/v1/01/${gtin}/21/${serial}`,
  { redirect: "manual" },
);

console.log(response.status);
console.log(response.headers.get("Location"));
console.log(response.headers.get("X-RateLimit-Remaining"));
```
```python
import requests

gtin = "03701234567891"
serial = "0000000000AB"

response = requests.get(
    f"https://api.sealtrust.io/v1/01/{gtin}/21/{serial}",
    allow_redirects=False,
    timeout=30,
)

print(response.status_code)
print(response.headers["Location"])
print(response.headers["X-RateLimit-Remaining"])
```
:::

> [!INFO] The TypeScript SDK does not cover this endpoint
> The `@sealtrust-io/sdk` package exposes batch minting, verification and
> notification subscriptions. So you call GS1 Digital Link resolution over plain
> HTTP, as above.

## Example response

HTTP code 302. The body is empty. Everything is in the `Location` header.

```http
HTTP/1.1 302 Found
Location: https://sealtrust.io/fr/product/0000000000AB
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Reset: 1786000020
Content-Length: 0
```

With `?linkType=dpp`, or for a unit withdrawn from the catalog, the destination
is the passport.

```http
HTTP/1.1 302 Found
Location: https://sealtrust.io/fr/passport/0x0000000000000000000000000000000000000000000000000000000000000000
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 598
X-RateLimit-Reset: 1786000020
Content-Length: 0
```

### What makes up the destination address

| Element | How we choose it |
| --- | --- |
| The host | The public SealTrust site. If your request arrived on the verified domain name of the owning brand, we keep the redirect on that domain, over `https`. |
| The language | `fr` or `en`. We read your `Accept-Language` header and we keep the first language in your list that is one of the two. If none matches, we answer `fr`. |
| The path | `product/{serial}` by default. `passport/{identifier}` when you ask for the passport, or when the unit is withdrawn from the catalog. |

`{identifier}` is the unit's public hash, a `0x` string followed by 64
hexadecimal characters. For a unit that has none, it is its token identifier,
and failing that its serial number. Do not rebuild it, read the `Location`
header.

Always read the `Location` header rather than rebuilding the address yourself.
The host and the language depend on the brand and on your request.

> [!INFO] The server always answers with a single redirect
> We resolve the language here, in this response. That is what avoids a second
> hop. The European digital product passport registry fetches identifier
> addresses in order to validate them and penalizes redirect chains.

> [!ATTENTION] A brand domain resolves only its own items
> If the request arrives on a brand's verified domain name and the unit belongs
> to another brand, the response is 404. A brand's domain therefore never serves
> a competitor's passport under its own visual identity.
>
> A brand domain is set up with us, then you have it verified from your console.
> As long as it is not verified, call this endpoint on `api.sealtrust.io`.

## Errors

| Code | Condition | What to do |
| --- | --- | --- |
| 400 | The path's GTIN contains no digit, or contains more than 14 once separators are stripped. Message `Invalid GTIN`. | Fix the GTIN. It must fit in 14 digits at most. |
| 400 | The path's GTIN does not have 8, 12, 13 or 14 digits, or its last digit is not the check digit of the preceding ones. Message `Invalid GTIN: the check digit does not match.` | Copy the GTIN from the reference's barcode, then call again. |
| 400 | Your request arrived on a domain name we do not serve, or on a brand domain that is not verified yet. The response is plain text, no JSON. | Call `https://api.sealtrust.io`, or have the brand's domain verified before using it. |
| 404 | The serial number does not have the expected form: length other than 12, or a character outside the Crockford Base32 alphabet. Message `Unknown GS1 Digital Link`. | Check the value read off the label. |
| 404 | No unit carries this serial number. Message `Unknown GS1 Digital Link`. | Check the value. If the item was never registered with us, this code is final. |
| 404 | The unit exists, its model carries no usable GTIN. Message `Unknown GS1 Digital Link`. | Fill in the model's GTIN in your console. In the meantime, use the short address `/p/{serial}`. |
| 404 | The unit exists, the path's GTIN is not the one of its model. Message `Unknown GS1 Digital Link`. | Rebuild the link from the model's real GTIN. The GTIN and serial number pair must designate the same object. |
| 404 | The request arrived on a brand's verified domain name, and the unit belongs to another brand. Message `Unknown GS1 Digital Link`. | Call this link on our domain, or on the domain of the brand that owns the item. |
| 404 | The path is incomplete, for example `/01/03701234567891/21` with no serial number. Message `Not Found`. | Complete the path. The four segments `01`, the GTIN, `21` and the serial number are all mandatory. |
| 429 | More than 60 calls in 60 seconds from the same IP address, across every `/01/` path. Message `Rate limit exceeded: 60 requests per 60s`. The response carries a `Retry-After` header in seconds. | Wait the number of seconds indicated by `Retry-After`, then try again. Spread out your calls instead of sending them in bursts. |
| 500 | Unexpected error on our side. Fixed body `{"detail": "Internal Server Error"}`. | Try again. The `X-Request-Id` header identifies the call; pass it on to us if it happens repeatedly. |

The five conditions that carry the message `Unknown GS1 Digital Link` return the
same code and the same body. You cannot tell them apart, and that is deliberate.
Distinct messages would let a scanner separate "this serial number does not
exist" from "this serial number exists under another GTIN", and therefore
confirm which numbers are real.

This endpoint does not return a 422. We take the GTIN and the serial number as
strings of characters, then we check them ourselves: an unusable GTIN, or one
whose check digit does not come out right, comes back as a 400, and a malformed
serial number comes back as a 404.

## See also

- [`GET /01/{gtin}`](/en/reference/get-gs1-gtin/),
  resolve a GS1 link that carries only a GTIN.
- [`GET /p/{serial}`](/en/reference/get-p-serial/),
  translate the printed serial number into a consumer page address.
- [`GET /passport/{identifier}`](/en/reference/get-passport-identifier/),
  read an item's published passport.
- [Physical identification, QR and NFC](/en/identification-physique/),
  choose the physical carrier and the exact form of the GS1 Digital Link.
