# GET /passport/{identifier}

Read a product's published digital passport from its printed number, its token identifier or its chip hash, at the requested access tier. Public endpoint.

Source: https://docs.sealtrust.io/en/reference/get-passport-identifier/

---

You read the published digital passport of a single product. By the end of this
page, you will know how to retrieve its data at the access tier you request,
read its warranty, know on what basis each section can be believed, and
recognize a product withdrawn from the catalog.

Full address:

```http
GET https://api.sealtrust.io/v1/passport/{identifier}
```

The same endpoint also answers without the `/v1` prefix, at
`https://api.sealtrust.io/passport/{identifier}`. Both addresses call the same
code. Use the `/v1` form for a new integration.

## Authorization

None for the `public` and `end_user` tiers. This endpoint is public.

A partner API key opens nothing here. The token received in the `Authorization`
header is decoded as a user account session token, and an API key is not one:
the read fails silently and the call proceeds as an anonymous call.

Four values of the `access_tier` parameter do, on the other hand, require an
account session, presented through the `Authorization: Bearer <session token>`
header or through the session cookie set at sign-in.

| Tier requested | What is needed |
| --- | --- |
| `public` | nothing |
| `end_user` | nothing |
| `repairer` | a session, and a repairer accreditation on the product's brand |
| `recycler` | a session, and a recycler accreditation on the product's brand |
| `upstream` | a session with access to the product's brand, or the authority role |
| `authority` | a session carrying the market surveillance authority role |

A session with access to the product's brand opens the three trade tiers on its
own products. A session carrying the market surveillance authority role opens
them as well.

> [!INFO] The product's current owner sees more
> If the call carries a valid session and the account is the unit's current
> owner, two things change. A request at the `public` tier is served at the
> `end_user` tier. Passports whose visibility is `owner_only` become visible.
> The `is_owner` field of the default JSON response and the `X-DPP-Access-Tier`
> header signal this switch.

## Rate limit

60 calls per 60-second window, counted per calling IP address. The window is
fixed. This limit is shared by every address starting with `/passport`, and the
`/passport/…` and `/v1/passport/…` forms feed the same counter.

Every accepted response carries three headers that describe this counter.

| Header | Content |
| --- | --- |
| `X-RateLimit-Limit` | the limit applied over the window, here `60` |
| `X-RateLimit-Remaining` | what is left to you in the current window |
| `X-RateLimit-Reset` | the timestamp of the end of the window, in seconds |

Going over returns 429, with the same three headers and a `Retry-After` in
seconds.

> [!INFO] This call consumes no quota
> An API key's daily quota is not eaten into by this call, and neither is your
> plan's monthly product quota. This endpoint queries neither one.

## Path and query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `identifier` | `string` | yes | The product's identifier. Three forms are accepted, see below. |
| `access_tier` | `string` | no | The access tier requested. Is `public` by default. Six accepted values, listed further down. |
| `verify_integrity` | `boolean` | no | Is `false` by default. At `true`, the server retrieves the passport's IPFS copy, compares its hash, and adds an `integrity` block to the response. |
| `format` | `string` | no | Absent by default, and the server then returns the JSON described below. The value `jsonld` returns the same filtered content, expressed in Schema.org and GS1. The server ignores any other value and returns the default response. |

### The three accepted identifier forms

| Form | What it looks like | Where you find it |
| --- | --- | --- |
| Serial number | 12 characters, digits and uppercase letters. The letters I, L, O and U never appear in it. | Printed on the product, it is what its QR code carries |
| Token identifier | A string of digits, often very long | Returned by our responses in the `token_id` field |
| Chip hash | `0x` followed by 64 hexadecimal characters | Returned by our responses in the `uid_hash` field |

The server recognizes the chip hash whatever the case. It recognizes the serial
number the same way, and it canonicalizes it the way the QR resolver does: it
reads the letters `I` and `L` as a `1`, and the letter `O` as a `0`. So you can
retype by hand the number read off a label, even if you confuse those
characters.

The server recognizes the form from the way it is written. It looks for a value
that starts with `0x` and is exactly 66 characters long as a chip hash. It looks
for any other value first as a token identifier, then, if that lookup gives
nothing, as a serial number.

### The six values of `access_tier`

These tiers are different audiences, with no hierarchy between them. A recycler
is not above a repairer. Each of the three trade tiers inherits the public tier
and the end user tier, then adds what its trade requires.

| Value | What it adds to the fields of `data` |
| --- | --- |
| `public` | product identification, ESPR compliance, REACH and CE marking, recyclability percentage, recycled content percentage, labels, general battery specification |
| `end_user` | environmental impact, full circularity, primary material, certified organic cotton, durability, energy efficiency, carbon footprint |
| `repairer` | bill of materials, disassembly instructions, repairability index, battery state of health |
| `recycler` | material composition, substances of concern, disassembly instructions, battery state of health |
| `upstream` | material composition, substances of concern, manufacturing, supply chain |
| `authority` | the entirety of the data, with no filtering |

A brand can replace these rules with its own, by product category. The table
above describes what applies in the absence of rules specific to the brand.

The server refuses any other value with a 422. It returns the tier actually
served in the `access_tier` field and in the `X-DPP-Access-Tier` header of the
default JSON response. Read one of the two rather than assuming it. With
`format=jsonld`, neither that field nor that header exists, see below.

### Response headers to know about

| Header | Content |
| --- | --- |
| `X-DPP-Access-Tier` | the tier actually served |
| `Cache-Control` | `no-store, max-age=0`, whatever the tier served. Do not put this response behind any shared cache. |

The server sets `X-DPP-Access-Tier` only on the default JSON response.
`Cache-Control` carries the same value on both formats.

No header is required in the request.

## Request body

None. This request has no body.

## Example request

Reading the public passport of the product whose printed number is
`EXEMP1E00001`. The three examples make the same call, stop the program on an
error response, then display the same three values:
`passport_version`, `access_tier` and `data.product_identity`.

:::onglets
```bash title="curl"
curl --fail-with-body -s \
  "https://api.sealtrust.io/v1/passport/EXEMP1E00001?access_tier=public" \
  | jq '{passport_version, access_tier, product_identity: .data.product_identity}'
```
```typescript
const identifiant = "EXEMP1E00001";

const url = new URL(
  `https://api.sealtrust.io/v1/passport/${encodeURIComponent(identifiant)}`,
);
url.searchParams.set("access_tier", "public");

const response = await fetch(url, { method: "GET" });

if (!response.ok) {
  throw new Error(`SealTrust a répondu ${response.status}`);
}

const passeport = await response.json();

console.log({
  passport_version: passeport.passport_version,
  access_tier: passeport.access_tier,
  product_identity: passeport.data.product_identity,
});
```
```python
import requests
from urllib.parse import quote

identifiant = "EXEMP1E00001"

response = requests.get(
    f"https://api.sealtrust.io/v1/passport/{quote(identifiant, safe='')}",
    params={"access_tier": "public"},
    timeout=30,
)
response.raise_for_status()

passeport = response.json()

print(
    {
        "passport_version": passeport["passport_version"],
        "access_tier": passeport["access_tier"],
        "product_identity": passeport["data"]["product_identity"],
    }
)
```
:::

In the `curl` example, `--fail-with-body` returns a non-zero exit code when the
server answers an error, and still displays the body. The `jq` tool only serves
to read the JSON in the terminal, it takes no part in the call.

> [!INFO] The TypeScript SDK does not cover this endpoint
> `@sealtrust-io/sdk` exposes no method for this address. The example above uses
> `fetch`, available with no dependency.

## Example response

HTTP code `200`.

This product is still in the catalog, it has not been claimed by a customer, and
the passport is requested at the public tier.

```json
{
  "id": 1,
  "product_id": 1,
  "brand_id": 42,
  "schema_version": "1.0",
  "passport_version": 3,
  "data": {
    "product_identity": {
      "gtin": "03701234567890",
      "model": "Cartable Exemple 32",
      "brand": "Exemple SAS",
      "made_in": "FR"
    },
    "compliance": {
      "eu_espr": true,
      "reach": true,
      "ce_marking": false
    },
    "circularity": {
      "recyclability_percentage": 62,
      "recycled_content_percentage": 0
    }
  },
  "data_hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "ipfs_uri": null,
  "ipfs_gateway_url": null,
  "visibility": "public",
  "access_tier": "public",
  "is_owner": false,
  "published_at": "2026-08-01T09:00:00+00:00",
  "product_name": "Cartable Exemple 32",
  "brand_name": "Exemple SAS",
  "image_url": "https://exemple-sas.test/images/cartable-32.jpg",
  "gtin": "03701234567890",
  "gs1_digital_link": "https://id.gs1.org/01/03701234567890/21/EXEMP1E00001",
  "warranty": {
    "status": "active",
    "ends_at": "2028-08-01T09:00:00+00:00",
    "duration_months": 24,
    "transferable": true,
    "remaining_days": 730
  },
  "evidence": {
    "sections": {
      "identity": "verified",
      "integrity": "declared",
      "composition": "declared",
      "substances_of_concern": "declared"
    },
    "legend": {
      "declared": "Stated by the brand. Recorded, dated and attributable, but not independently checked.",
      "verified": "Checked mechanically against a public record; no declaration involved."
    },
    "derived": true,
    "note": "Statuses are computed by SealTrust from verifiable facts. A brand cannot set them."
  }
}
```

The domain of the `gs1_digital_link` field is that of the resolver configured for
your integration. `https://id.gs1.org` is only the fallback value, used when no
resolver is configured. Do not hard-code this domain, read the value returned.

### The fields of the response

| Field | Type | Description |
| --- | --- | --- |
| `id` | `integer` | The identifier of the passport version served. |
| `product_id` | `integer` | The unit to which this passport is attached, or `null` when the passport covers the model and applies to all of its units. |
| `brand_id` | `integer` | The number of the brand the passport belongs to. |
| `schema_version` | `string` | The version of the passport's data schema. |
| `passport_version` | `integer` | The published version number. It goes up with each new publication. |
| `data` | `object` | The passport's data, filtered according to the tier served. Its shape depends on the product category. |
| `data_hash` | `string` or `null` | The hash of this version's complete data, 64 hexadecimal characters. `null` when no hash has been recorded for this version. |
| `ipfs_uri` | `string` | The IPFS address of the passport's copy. Always `null` at the `public` and `end_user` tiers. |
| `ipfs_gateway_url` | `string` | The HTTP address through which this copy is read. Always `null` at the `public` and `end_user` tiers. |
| `visibility` | `string` | The visibility of the version served: `public`, or `owner_only` when the current owner is authenticated. The `brand_only` visibility is never served here. |
| `access_tier` | `string` | The tier actually served, which may differ from the tier requested for the product's owner. |
| `is_owner` | `boolean` | `true` when the call is authenticated and the account is the unit's current owner. |
| `published_at` | `string` | Date and time this version was published, in ISO 8601 format, or `null`. |
| `product_name` | `string` or `null` | The product's name. `null` when no name has been recorded on the item. |
| `brand_name` | `string` | The brand's name, or `null` if the product is attached to none. |
| `image_url` | `string` | The model's photograph, or `null`. |
| `gtin` | `string` | The model's GTIN, brought back to 14 digits. `null` when the model carries none, or when the recorded value is not a valid GTIN. |
| `gs1_digital_link` | `string` | The GS1 Digital Link that identifies this unit, of the form `<resolution domain>/01/<14-digit gtin>/21/<serial number>`. `null` when the GTIN or the serial number is missing. |
| `warranty` | `object` | The warranty summary, or `null` when the product has none. See below. |
| `evidence` | `object` | On what basis each section can be believed. See below. Absent if its computation fails. |
| `lifecycle` | `object` | Present only when the unit is destroyed or out of the catalog. See below. |
| `integrity` | `object` | Present only when `verify_integrity=true` and the IPFS link is served at your tier. See below. |

### The `warranty` block

| Field | Type | Description |
| --- | --- | --- |
| `status` | `string` | `active`, `expiring_soon`, `expired` or `void`. Recomputed on each read. |
| `ends_at` | `string` | End date, in ISO 8601 format, or `null` for a lifetime warranty. |
| `duration_months` | `integer` | The announced duration, in months. |
| `transferable` | `boolean` | `true` when the warranty follows the product on a change of owner. |
| `remaining_days` | `integer` | Whole days remaining. Negative when the warranty has passed. `null` for a lifetime or voided warranty. |

### The `evidence` block

Three values exist, and they are computed by SealTrust. A brand cannot choose
them.

| Value | What it says |
| --- | --- |
| `verified` | Checked mechanically against a public record, with no declaration from anyone. |
| `document_backed` | A third-party document is attached and can be retrieved. Its content has not been audited by SealTrust. |
| `declared` | Declared by the brand. Recorded, dated, attributable, not independently verified. |

Four sections carry one of these values: `identity`, `integrity`, `composition`
and `substances_of_concern`. The `identity` section goes to `verified` when the
unit carries a token identifier on the chain. The `integrity` section goes to
`verified` when this version's hash has been anchored and still matches the
recorded data. The block additionally carries `legend`, which restates the
meaning of the values present, `derived` at `true`, and `note`.

### The `lifecycle` block

It appears only if the unit is destroyed or out of the catalog. Its passport is
still served so that the identifier keeps resolving.

```json
{
  "lifecycle": {
    "status": "superseded",
    "is_burned": false,
    "superseded": true,
    "note": "This unit is superseded or withdrawn; its passport is retained so the identifier stays resolvable (EN 18219 §4.2.2 persistence)."
  }
}
```

Read `is_burned` before `status`.

The `status` field is `superseded` when the unit has been replaced by another,
and `archived` when it was withdrawn without replacement. The `superseded` field
is `true` only for the first case.

The block also appears when the unit has been destroyed, that is, when
`is_burned` is `true`. In that case `status` carries the product's current
state, which may be `null` or an active value. So never infer destruction from
the value of `status`.

### The `integrity` block

```json
{
  "integrity": {
    "ipfs_fetched": true,
    "ipfs_hash": "1111111111111111111111111111111111111111111111111111111111111111",
    "expected_hash": "1111111111111111111111111111111111111111111111111111111111111111",
    "match": true
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `ipfs_fetched` | `boolean` | `true` when the server managed to read the IPFS copy. |
| `ipfs_hash` | `string` | The hash of the content actually read on IPFS. |
| `expected_hash` | `string` | The hash expected for this content. |
| `match` | `boolean` | The verdict of the comparison. |

`expected_hash` is the hash of the PUBLIC projection of the passport, the one
deposited on IPFS. It differs from `data_hash`, which covers the complete data,
including the fields reserved for the professional tiers. The two values coincide
only when the passport carries no non-public field. So never compare
`expected_hash` and `data_hash`.

The `match` field is `true` when the IPFS copy matches, `false` when it differs,
and `null` when the copy could not be retrieved. In that last case
`ipfs_fetched` is `false`, an `error` field replaces the two hashes, and `null`
means that nothing could be concluded.

> [!ATTENTION] `verify_integrity=true` does nothing at the open tiers
> The server computes this block only if it serves you the IPFS link. At the
> `public` and `end_user` tiers, it never serves that link. So it accepts the
> parameter, and it returns a response with no `integrity` block. No error
> signals this to you.

### The JSON-LD response

With `format=jsonld`, the response carries the content type
`application/ld+json`. It is a Schema.org and GS1 document whose fields are
filtered by the same access tier. It contains neither `passport_version`, nor
`data_hash`, nor the `warranty`, `evidence`, `lifecycle` and `integrity` blocks
described above: the warranty is expressed there as `WarrantyPromise`, and the
other blocks do not appear in it.

Two other differences matter for your integration.

The document carries no `access_tier` field. The server does not return the
`X-DPP-Access-Tier` header either. To know the tier actually served, call
without `format`, or stick to the tier you requested.

The server ignores `verify_integrity` in this format. It returns the JSON-LD
document before computing the `integrity` block, so this parameter changes
nothing in the response and no error signals it to you.

## Errors

The body of an error response carries a `detail` field.

| Code | Condition | What to do |
| --- | --- | --- |
| 401 | `access_tier=authority` is requested with no valid session. `detail` is `Authority-tier access requires authentication`. | Sign in with an account carrying the market surveillance authority role. A partner API key will not do. |
| 401 | `access_tier` is `repairer`, `recycler` or `upstream`, and the call carries no valid session. `detail` is `Professional-tier access requires authentication`. | Present an account session token, or request the `public` or `end_user` tier. |
| 403 | `access_tier=authority` is requested by a signed-in account that does not carry that role. `detail` is `Authority-tier access is restricted to market surveillance authorities`. | Request the tier that matches your authorization. |
| 403 | A professional tier is requested by a signed-in account that has neither access to the product's brand nor the matching accreditation on that brand. `detail` is `This tier is restricted to the product's brand, partners holding the matching accreditation, and market-surveillance authorities`. | Ask the brand for the accreditation that matches your trade, then request the tier of that trade. |
| 404 | No product matches the identifier, under any of the three accepted forms. `detail` is `Product not found`. | Check the number you copied. A destroyed product or one withdrawn from the catalog is still resolved here, so this response does mean that the identifier is unknown. |
| 404 | The product exists, but no published passport version matches it. `detail` is `No published passport found for this product`. | The brand must publish a version. An unpublished draft is never served, and neither is a version with `brand_only` visibility. |
| 422 | A parameter value is refused: an `access_tier` that is not one of the six values, or a `verify_integrity` that is not a boolean. `detail` is a list, each entry carrying `loc`, `type` and `msg`. | Read `loc` to find out which parameter is at fault, then fix its value. |
| 429 | More than 60 calls have been made from your IP address to a `/passport` address within the current 60-second window. `detail` is `Rate limit exceeded: 60 requests per 60s`. | Wait the number of seconds indicated by the `Retry-After` header, then try again. |
| 500 | An unexpected error occurred while processing your call. `detail` is `Internal Server Error`. | Try again. If the error persists, contact support giving the time of the call and the value of the response's `X-Request-Id` header. |

> [!INFO] The order of the checks decides the code returned
> The server checks the `authority` tier first, before it even looks for the
> product. An unknown identifier requested with `access_tier=authority` and with
> no session therefore receives a 401. The server checks the three trade tiers
> after having looked for the product and its passport. The same unknown
> identifier requested with `access_tier=repairer` therefore receives a 404.

## See also

- [`GET /passport/01/{gtin}`](/en/reference/get-passport-gtin/),
  read a model's published passport, from its GTIN.
- [`GET /passport/{identifier}/verify`](/en/reference/get-passport-verify/),
  check the integrity of an item's published passport.
- [`GET /passport/{identifier}/proof`](/en/reference/get-passport-proof/),
  gather the public proofs of an item's passport.
- [`GET /passport/{identifier}/vc`](/en/reference/get-passport-vc/),
  retrieve the passport's signed credential, in SD-JWT-VC format.
- [Publishing a digital product passport](/en/passeport-dpp/),
  publish, choose who sees which fields, export and have it verified.
