# Publish a digital product passport

Publish a passport, choose who sees which fields, export it as JSON-LD and have it verified independently.

Source: https://docs.sealtrust.io/en/passeport-dpp/

---

When you leave this page, you will know what a SealTrust passport contains, how
to publish it from the console, how its content is filtered according to the
person reading it, how to retrieve it as JSON or as JSON-LD, and how a third
party verifies its signature without trusting us.

Every address quoted here is public. Reading a passport at the public tier
requires no API key.

## What a passport covers

A passport attaches either to a product model or to a specific item.

The **reference** passport is attached to the model. It is shared by every item
of the model. It is the level that suits most products, and it does not require
serializing every item.

A batch is covered by the reference passport of its model. The ESPR regulation
allows a passport at model, batch or item level. SealTrust attaches a passport
to a model or to a unit, so batch production is covered by the model's reference
passport: it holds for every item that shares the same product code. You do not
have to create one passport per item in order to produce in batches. Only
certain batteries must carry a per-item passport, under Regulation (EU)
2023/1542.

The **item** passport is attached to a physical unit. It carries the data
specific to that unit, for example a battery state of health or a declaration
tied to its serial number.

When you ask for a unit's passport, the server first looks for a passport
attached to that unit. If it finds none, it serves the reference passport of its
model. It never serves in its place a passport attached to another unit of the
same model.

> [!INFO] One identifier, three forms
> The `identifier` field accepts a UID hash (`0x` followed by 64 hexadecimal
> characters), a token id (a string of digits), or the serial number printed on
> the label (12 characters). The serial number is read without regard to case,
> and ambiguous characters are brought back to their canonical form: `I` and `L`
> count as `1`, `O` counts as `0`.

## What a passport contains

The passport content is a free-form JSON document, organized in sections. The
access rules are written against the paths of those sections, which fixes the
list of names used:

| Section | Content |
| --- | --- |
| `product_identity` | GTIN, model, brand, country of manufacture, production site |
| `labels` | labels and markings carried on the product |
| `compliance` | ESPR and REACH compliance, CE marking |
| `circularity` | recyclability, recycled content, repairability index, disassembly instructions |
| `environmental_impact` | carbon footprint, water, energy, transport |
| `carbon_footprint` | detailed carbon footprint |
| `energy_efficiency` | energy class, EPREL registration |
| `durability` | expected service life |
| `materials` | material composition |
| `substances_of_concern` | substances of concern and safety data sheets |
| `bill_of_materials` | bill of components |
| `manufacturing` | manufacturing data |
| `supply_chain` | supply chain |
| `battery_specification` | general specification of a battery |
| `state_of_health` | state of health of a battery |

The last two sections come from the batteries regulation (EU) 2023/1542. Their
presence in a passport is enough to classify it as a battery passport when
choosing the access rules.

Around this data, the per-unit read also carries elements the server computes
itself: the product name and the brand name, the model photo, the GTIN, the GS1
Digital Link, a warranty summary, and an information provenance block described
below. Reading a model's reference passport carries fewer of them, the exact
list appears below.

## Publishing a passport

In the console, open **DPP Compliance**, then the **DPP Passports** tab. There
you create a version, you fill it in, and you publish it.

Four things happen at the moment you publish.

**The other published versions of the same model stop being published.** They
stay recorded, their publication date is cleared and their visibility moves to
"brand only".

**The server seals the version.** Its content becomes immutable. The server
refuses any change to the content of a sealed version, with code 409. To correct
a piece of data, you publish a new version. Unpublishing a version does not
unseal it.

**The server deposits a public copy on IPFS.** It deposits only the public
projection of the passport. The fields reserved for the professional tiers do
not appear in it. If the deposit fails, publication still succeeds and the
passport remains without an IPFS copy.

**The server issues or refreshes the signed credential.** The signed passport
automatically follows the published version, with no extra step. If signing
fails, publication still succeeds and the passport remains without a credential.

> [!DANGER] Publishing a unit unpublishes the model
> As soon as the product belongs to a model, the server also attaches the new
> version to that model. Unpublishing then bears on the whole model: publishing
> a passport attached to a single unit unpublishes the model's reference
> passport and the passports of every other unit of that model. Those passports
> move back to "brand only" visibility and the endpoint
> `GET /v1/passport/01/{gtin}` returns 404. Check this consequence before
> publishing an item passport on a model that already carries a reference
> passport.

> [!ATTENTION] The trial account does not get the passport
> The passport is included in every paid plan. A trial account is not entitled
> to it: creating and modifying a version then return a refusal carrying the
> code `FEATURE_NOT_AVAILABLE`.

Each version carries a number that increments. As soon as the product belongs to
a model, the server counts that number per model. It counts it per unit only for
a product that belongs to no model.

Three visibilities exist: `public`, `owner_only` and `brand_only`. The server
serves only `public` to an anonymous visitor. It additionally serves
`owner_only` to the unit's current owner when that owner is authenticated. It
never serves `brand_only` through the public route.

## Reading the passport

The endpoint is `GET /v1/passport/{identifier}`.

:::onglets
```bash title="curl"
curl "https://api.sealtrust.io/v1/passport/EXEMP1E00001"
```
```typescript
const response = await fetch(
  "https://api.sealtrust.io/v1/passport/EXEMP1E00001",
);
const passeport = await response.json();
console.log(passeport.passport_version, passeport.access_tier);
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/passport/EXEMP1E00001",
    timeout=10,
)
response.raise_for_status()
passeport = response.json()
print(passeport["passport_version"], passeport["access_tier"])
```
:::

The response, at the public tier, has this form. The values are made up.

```json title="200 OK"
{
  "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",
      "production_facility": "Atelier Exemple Nord"
    },
    "compliance": {
      "eu_espr": true,
      "reach": true
    },
    "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 that domain, read the value the
response returns.

Two blocks appear only when there is reason for them. `lifecycle` appears when
the unit has been replaced or withdrawn: the server keeps serving its passport
so that the identifier always resolves. `integrity` appears when you add the
`verify_integrity=true` parameter and you call at a professional or authority
tier. That block is described below.

### The information provenance block

The `evidence` block says on what basis each part of the passport can be
believed. Three values exist:

- `verified`: checked mechanically against a public record, with no declaration
  involved.
- `document_backed`: a third-party document is attached and can be retrieved.
  Its content has not been audited.
- `declared`: stated by the brand. Recorded, dated, attributable, not
  independently checked.

SealTrust computes these values. A brand cannot choose them. The `identity`
section moves to `verified` when the unit exists on the chain. The `integrity`
section moves to `verified` when the hash of this version has been anchored and
still matches the recorded data.

### A model's reference passport

To read the passport that the GTIN announces for the model, use
`GET /v1/passport/01/{gtin}`.

```bash title="curl"
curl "https://api.sealtrust.io/v1/passport/01/03701234567890"
```

The response has this form, and it has no other. The values are made up.

```json title="200 OK"
{
  "id": 7,
  "product_id": null,
  "product_model_id": 4,
  "gtin": "03701234567890",
  "level": "model",
  "brand_id": 42,
  "schema_version": "1.0",
  "passport_version": 2,
  "data": {
    "product_identity": {
      "gtin": "03701234567890",
      "model": "Cartable Exemple 32",
      "brand": "Exemple SAS",
      "made_in": "FR",
      "production_facility": "Atelier Exemple Nord"
    },
    "compliance": {
      "eu_espr": true,
      "reach": true
    },
    "circularity": {
      "recyclability_percentage": 62,
      "recycled_content_percentage": 0
    }
  },
  "data_hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "ipfs_uri": "ipfs://bafyexemple0000000000000000000000000000000000000000000",
  "visibility": "public",
  "published_at": "2026-08-01T09:00:00+00:00",
  "product_name": "Cartable Exemple 32",
  "brand_name": "Exemple SAS"
}
```

This endpoint returns fewer fields than the per-unit read. The response contains
no owner, no end-of-life banner and no warranty: none of these three exists at
model level. It also does not contain the model photo, the GS1 Digital Link, the
information provenance block, the `is_owner` field, or the `access_tier` field.
If your integration reads one of these fields, read the passport per unit.

Here the server returns the `ipfs_uri` field to every caller, including
anonymous ones, whatever tier is requested. Treat the content of that IPFS copy
as public. This endpoint never returns an `ipfs_gateway_url` field.

The `access_tier` parameter works here as it does on the per-unit read: it
filters the content of `data`, and the professional and authority tiers require
the same authentication. The `format=jsonld` parameter and the
`verify_integrity` parameter, for their part, do not exist on this endpoint.

## Access tiers, who sees what

The `access_tier` parameter decides which fields are returned. It accepts six
values.

| Tier | What it adds | Who gets it |
| --- | --- | --- |
| `public` | identification, labels, ESPR / REACH / CE compliance, recyclability and recycled content, general battery specification | everyone, without authentication |
| `end_user` | environmental impact, full circularity, primary material, durability, energy efficiency, carbon footprint | everyone, without authentication |
| `repairer` | bill of materials, disassembly instructions, repairability index, battery state of health | authenticated account accredited as a repairer on the brand |
| `recycler` | material composition, substances of concern, disassembly instructions, battery state of health | authenticated account accredited as a recycler on the brand |
| `upstream` | material composition, substances of concern, manufacturing, supply chain | authenticated account of the brand, or an authority |
| `authority` | the entire data set | authenticated account carrying the market surveillance authority role |

> ⚠️ These six values are not ordered from narrowest to widest. `repairer`,
> `recycler` and `upstream` are three distinct audiences: each one adds, to the
> consumer baseline, what its own trade requires, and none of them contains
> another's fields. An accredited recycler therefore does not read the bill of
> materials reserved for the repairer, and holding one accreditation opens none
> of the others. Only `authority` receives the entire set.

The three professional tiers and the authority tier require authentication. It
is a user account session, presented either through the
`Authorization: Bearer <jeton de session>` header, or through the session cookie
set at login. A partner API key does not open these tiers.

Three of these tiers correspond to a trade: `repairer`, `recycler` and
`upstream`. They are not ranked one above another. A recycler is not above a
repairer. Each inherits the public tier and the end-user tier, then adds what
its trade requires. The accreditations issued to a partner are of two kinds,
repairer and recycler, so no accreditation opens `upstream` by itself.

:::schema profils-acces
A common base occupies the top of the diagram, served without authentication:
the `public` tier carries identification, labels, compliance, recyclability and
the general battery specification, then the `end_user` tier adds environmental
impact, circularity, durability and carbon footprint. From that base start three
arrows, toward three cards of the same size placed at the same height. The
first, `repairer`, goes to an account accredited as a repairer on the brand and
adds the bill of materials, the disassembly instructions, the repairability
index and the battery state of health. The second, `recycler`, goes to an
account accredited as a recycler and adds material composition, substances of
concern, disassembly instructions and battery state of health. The third,
`upstream`, goes to the brand team or to an authority, and adds material
composition, substances of concern, manufacturing and supply chain. Between each
pair of cards, a cross-shaped sign recalls that none of the three receives what
its neighbor adds. A separate card at the bottom carries the market surveillance
`authority` tier, which receives the entire data set whatever the trade.
:::

> [!ATTENTION] A trade tier does not wall a field off from the others
> Filing a field under the `repairer` tier makes it readable by every partner
> carrying the repairer accreditation on your brand, by your team and by an
> authority. If a field must reach only your team and an authority, remove that
> field from the passport.

Ask for the tier of your trade. It is the one that describes what you are, and
it returns what your trade requires.

```bash title="curl"
curl -H "Authorization: Bearer JETON_DE_SESSION_FICTIF" \
  "https://api.sealtrust.io/v1/passport/EXEMP1E00001?access_tier=repairer"
```

Refusals are explicit:

| Code | Condition |
| --- | --- |
| 401 | professional or authority tier requested without authentication |
| 403 | authenticated, but without the matching accreditation on that brand, and without access to the brand |
| 403 | `authority` tier requested by an account that does not carry that role |
| 404 | product not found, or no passport published for that product |

JSON responses carry the `X-DPP-Access-Tier` header with the tier actually
served. Read it rather than assuming. The JSON-LD export, described below, does
not carry that header.

Two behaviors to know about.

**The current owner sees more.** If the call carries a valid session and the
account is the unit's current owner, the server answers at the `end_user` tier
to a request at the `public` tier, and it additionally serves passports with
`owner_only` visibility. The `is_owner` field and the `X-DPP-Access-Tier` header
signal that change. This elevation holds only for the JSON and JSON-LD reads, it
does not apply to the signed credential.

**No response is cached.** The server serves every response with
`Cache-Control: no-store, max-age=0`, whatever tier is requested and whatever
the format. A shared cache therefore never returns a privileged body to the next
caller. To reduce the number of your calls, keep the result in your own
application cache, with the freshness window your usage tolerates.

**The IPFS link depends on the tier, and on the endpoint.** On
`GET /v1/passport/{identifier}`, the `ipfs_uri` and `ipfs_gateway_url` fields
stay null at the `public` and `end_user` tiers. The server fills them in only at
the professional and authority tiers, which are authenticated. On
`GET /v1/passport/01/{gtin}`, the server returns `ipfs_uri` to every caller,
including anonymous ones.

## Defining your own access rules

By default, the distribution of fields between tiers is the one described above.
You can replace it, brand by brand, in **DPP Compliance** then **Access rules**.
A rule associates a field path with a tier. Paths accept the wildcard character,
for example `materials.*`.

Rules can be carried per product group. The rules of the `general` group apply
everywhere; the rules of the `battery` group apply in addition to battery
passports.

> [!DANGER] A single rule replaces every default
> As soon as your brand owns at least one rule, the built-in rules are no longer
> used at all for that brand. A field that none of your rules covers is no
> longer returned to anyone, except at the `authority` tier. Before writing your
> first rule, use the action that copies the defaults as rules, then change what
> needs changing. That action overwrites nothing, it only adds what is missing.

Deleting all of a brand's rules at once requires retyping a confirmation
sentence. That sentence contains the exact number of rules that would be deleted
and the scope of the operation. The screen that lists the rules supplies it to
you. Without it, the deletion is refused.

This confirmation exists because the operation is a confidentiality change on
regulated data. The fields you had restricted revert to the built-in rules,
which can make them publicly readable.

## JSON-LD export

Add `format=jsonld` to get the passport as linked data, in Schema.org and GS1
vocabulary. The server returns this response with the content type
`application/ld+json`.

:::onglets
```bash title="curl"
curl -H "Accept: application/ld+json" \
  "https://api.sealtrust.io/v1/passport/EXEMP1E00001?format=jsonld"
```
```typescript
const response = await fetch(
  "https://api.sealtrust.io/v1/passport/EXEMP1E00001?format=jsonld",
  { headers: { Accept: "application/ld+json" } },
);
const jsonld = await response.json();
console.log(jsonld["@context"], jsonld.name);
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/passport/EXEMP1E00001",
    params={"format": "jsonld"},
    headers={"Accept": "application/ld+json"},
    timeout=10,
)
response.raise_for_status()
jsonld = response.json()
print(jsonld["@context"], jsonld["name"])
```
:::

```json title="200 OK"
{
  "@context": {
    "@vocab": "https://schema.org/",
    "gs1": "https://gs1.org/voc/",
    "espr": "https://data.europa.eu/espr/"
  },
  "@type": "Product",
  "identifier": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "gtin": "03701234567890",
  "name": "Cartable Exemple 32",
  "brand": {
    "@type": "Brand",
    "name": "Exemple SAS",
    "url": "https://exemple-sas.test"
  },
  "countryOfOrigin": "FR",
  "gs1:productionFacility": "Atelier Exemple Nord",
  "material": [],
  "additionalProperty": [
    {
      "@type": "PropertyValue",
      "name": "Recyclability (EN 45555)",
      "value": 62,
      "unitText": "percent"
    },
    {
      "@type": "PropertyValue",
      "name": "gs1:recycledContentPercentage",
      "value": 0,
      "unitText": "percent"
    }
  ],
  "espr:compliance": {
    "@type": "espr:ComplianceDeclaration",
    "espr:euEsprCompliant": true,
    "espr:reachCompliant": true
  },
  "warranty": {
    "@type": "WarrantyPromise",
    "durationOfWarranty": {
      "@type": "QuantitativeValue",
      "value": 24,
      "unitCode": "MON"
    },
    "espr:warrantyStatus": "active",
    "espr:warrantyEndDate": "2028-08-01T09:00:00+00:00",
    "espr:transferable": true
  }
}
```

Four points to remember.

The JSON-LD export is built **after** the tier filtering. An anonymous call
therefore gets the public projection in JSON-LD, and nothing more. To get the
professional fields in JSON-LD, add `access_tier` and authenticate.

Key names change between the two formats. The `compliance` section of the JSON
becomes an `espr:compliance` node of type `espr:ComplianceDeclaration`, whose
keys are `espr:euEsprCompliant`, `espr:reachCompliant`, `espr:ceMarking` and
their neighbors. Do not expect the names of the JSON response in the JSON-LD.

The server removes keys without a value from the document. A field you have not
filled in does not appear as `null`.

This response does not carry the `X-DPP-Access-Tier` header, unlike the JSON
response. The tier served is the one you asked for in `access_tier`.

The `format` parameter exists only on `GET /v1/passport/{identifier}`. The
endpoint for a GTIN's reference passport does not offer it.

## The signed passport and its verification

The server also issues every published passport as a verifiable credential, in
SD-JWT-VC format. That credential lets a third party check that the brand did
issue this passport, without going through our verification endpoint and without
granting us any trust.

### The issuer

The issuer is a `did:web` decentralized identifier carried by the brand. Its
default form is `did:web:api.sealtrust.io:brand:{brand_id}`, which resolves, per
the did:web specification, to the address
`https://api.sealtrust.io/brand/{brand_id}/did.json`.

```bash title="curl"
curl "https://api.sealtrust.io/brand/4242/did.json"
```

The document returned lists all the brand's non-revoked public keys, each as a
`JsonWebKey2020`. The identifier of a key is `<did>#key-<version>`, which lets
an old credential stay verifiable after a key rotation, as long as the old
version is not revoked.

A brand can also carry its identifier on its own domain. The document is then
found at `https://<domaine de la marque>/.well-known/did.json`, and the
identifier takes the form `did:web:<domaine de la marque>`. The brand thus keeps
ownership of its issuer identity.

> [!INFO] Where the private key lives
> The signing private key is never returned by the API. We publish only the
> public part, the one the issuer document carries, and that is what you use to
> verify.

### Retrieving the credential

`GET /v1/passport/{identifier}/vc` returns the presentation filtered according
to the tier requested. This route applies the same tier grid as the JSON read,
with two differences:

- the automatic owner elevation does not apply. An authenticated owner stays at
  the tier they ask for, and the server does not serve passports with
  `owner_only` visibility;
- a replaced or withdrawn unit returns 404, whereas the JSON read keeps serving
  its passport with the `lifecycle` banner.

These two differences hold for the three addresses of the credential: `/vc`,
`/vc/verify` and `/vc/preview`.

```bash title="curl"
curl "https://api.sealtrust.io/v1/passport/EXEMP1E00001/vc"
```

```json title="200 OK"
{
  "passport_id": 1,
  "issuer": "did:web:api.sealtrust.io:brand:4242",
  "vct": "https://schema.sealtrust.io/vct/digital-product-passport",
  "access_tier": "public",
  "format": "dc+sd-jwt",
  "sd_jwt_vc": "eyJFWEVNUExFIn0.eyJFWEVNUExFIn0.RVhFTVBMRQ~WyJFWEVNUExFIl0~"
}
```

Disclosure is selective. The server leaves the fields of the public tier in the
clear and replaces the others with hashes. It reveals a hash only if the tier
requested entitles you to it.

Two blocks escape this rule. The server always carries them in the clear,
whatever tier is requested, and it never masks them:

- the brand identity: legal name, LEI identifier, EORI number, website, postal
  address and contact email;
- the unit identity: UID hash, token id and product name.

A presentation at the public tier therefore reveals the public projection, plus
these two blocks. The JSON read at the public tier, for its part, returns only
the brand's name. Check the content of your brand record before distributing
credentials: its postal address and its contact email go with them.

If no credential has been issued yet for this passport, the response is 404.

### Verifying the credential

Two paths exist, and the second does not depend on us.

**Through our endpoint.** `GET /v1/passport/{identifier}/vc/verify` checks the
presentation matching the tier requested against the brand's public key.

:::onglets
```bash title="curl"
curl "https://api.sealtrust.io/v1/passport/EXEMP1E00001/vc/verify"
```
```typescript
const response = await fetch(
  "https://api.sealtrust.io/v1/passport/EXEMP1E00001/vc/verify",
);
const result = await response.json();
console.log(result.verified, result.issuer);
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/passport/EXEMP1E00001/vc/verify",
    timeout=10,
)
response.raise_for_status()
result = response.json()
print(result["verified"], result["issuer"])
```
:::

```json title="200 OK"
{
  "passport_id": 1,
  "issuer": "did:web:api.sealtrust.io:brand:4242",
  "vct": "https://schema.sealtrust.io/vct/digital-product-passport",
  "key_version": 1,
  "access_tier": "public",
  "verified": true,
  "error": null,
  "credential_subject": {
    "product_identity": {
      "gtin": "03701234567890",
      "model": "Cartable Exemple 32",
      "brand": "Exemple SAS",
      "made_in": "FR",
      "production_facility": "Atelier Exemple Nord"
    }
  }
}
```

On failure, `verified` is `false`, `error` is `verification_failed` and
`credential_subject` is null. The original error message is never returned.

**By your own means.** Retrieve the presentation with
`GET /v1/passport/{identifier}/vc`, retrieve the issuer document at the
`did.json` address given above, choose the key whose identifier matches the
`kid` field of the credential header, and check the signature. The signature
algorithm is `ES256`, and the type declared in the header is `dc+sd-jwt`. Any
standard did:web and SD-JWT-VC library is enough.

### Seeing the content before signing

`GET /v1/passport/{identifier}/vc/preview` returns the envelope of the
credential and its content for a given tier, without a signature. The response
carries `"signed": false`.

The preview returns the content of the tier requested in JSON-LD format. The
signed credential, for its part, carries the raw data tree of the passport, with
the section names of the JSON response. Use the preview to check which fields a
given tier sees. Its structure is not that of the credential, so do not use it
as an integration template.

## Integrity proofs

Two public endpoints accompany the passport. They require no authentication.

### The integrity check

`GET /v1/passport/{identifier}/verify` recomputes the hash of the data, compares
it with the recorded one, compares the IPFS copy when it exists, and recomputes
the version hash of a sealed version.

```json title="200 OK"
{
  "db_hash_match": true,
  "ipfs_match": true,
  "ipfs_uri": "ipfs://bafyexemple0000000000000000000000000000000000000000000",
  "ipfs_gateway_url": "https://ipfs.io/ipfs/bafyexemple0000000000000000000000000000000000000000000",
  "data_hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "computed_hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "passport_version": 3,
  "seal": {
    "sealed": true,
    "sealed_at": "2026-08-01T09:00:00+00:00",
    "algorithm": "st-dpp-chain-v1",
    "version_hash": "0000000000000000000000000000000000000000000000000000000000000000",
    "prev_version_hash": "0000000000000000000000000000000000000000000000000000000000000000",
    "linked": true,
    "chain_link_match": true
  }
}
```

Be precise about what each line establishes.

`db_hash_match` compares data we hold with a hash we hold. It is an internal
consistency check.

`ipfs_match` compares the public copy deposited on IPFS with the expected public
projection. The copy is addressed by its hash, so it cannot be modified without
changing address. When the copy cannot be retrieved, the value is `null`, which
means "unknown". It never flips to `false` for a mere gateway outage.

`version_hash` carries the hash of this version, 64 hexadecimal characters.
`prev_version_hash` carries the hash of the previous version, and it is `null`
for the first sealed version. Each version hash covers the one before it, so
rewriting a version detaches every version published after it.

`chain_link_match` is the tampering signal. If `chain_link_match` is `false`,
the version was modified after its publication. A passport published before this
chaining existed carries `linked: false` and `reason: sealed_before_chain`, and
no version hash is manufactured after the fact for a publication we cannot date.

The server returns the IPFS link here only if the deposited copy is proven
identical to the public projection. Otherwise, it removes the link from the
response.

### The proof summary

`GET /v1/passport/{identifier}/proof` gathers everything a third party can
check. The same summary exists for a reference passport at
`GET /v1/passport/01/{gtin}/proof`.

```json title="200 OK"
{
  "passport_version": 3,
  "data_hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "ipfs_uri": "ipfs://bafyexemple0000000000000000000000000000000000000000000",
  "ipfs_gateway_url": "https://ipfs.io/ipfs/bafyexemple0000000000000000000000000000000000000000000",
  "anchor": {
    "chain": "base",
    "chain_id": 8453,
    "type": "merkle_batch",
    "tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "basescan_url": "https://basescan.org/tx/0x0000000000000000000000000000000000000000000000000000000000000000",
    "merkle_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "anchored": true,
    "proves": "batch_inclusion"
  },
  "passport_anchor": {
    "chain": "base",
    "chain_id": 8453,
    "tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "basescan_url": "https://basescan.org/tx/0x0000000000000000000000000000000000000000000000000000000000000000",
    "merkle_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "leaf": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "leaf_index": 0,
    "proof": [],
    "anchored_at": "2026-08-01T10:00:00+00:00",
    "data_hash_matches": true,
    "proves": "content_existed_at_or_before_tx"
  },
  "seal": {
    "sealed": true,
    "sealed_at": "2026-08-01T09:00:00+00:00",
    "algorithm": "st-dpp-chain-v1",
    "version_hash": "0000000000000000000000000000000000000000000000000000000000000000",
    "prev_version_hash": "0000000000000000000000000000000000000000000000000000000000000000",
    "linked": true,
    "chain_link_match": true
  },
  "vc": {
    "issued": true,
    "vct": "https://schema.sealtrust.io/vct/digital-product-passport",
    "issued_at": "2026-08-01T09:00:00+00:00"
  },
  "verifications": {
    "count": 12,
    "last_verified_at": "2026-08-12T14:32:00+00:00"
  }
}
```

Two blocks carry an on-chain reference, and they do not say the same thing.

`anchor` dates **the item**. Read the `anchored` field. The name of the key is
not enough.
When `anchored` is `true`, `type` is `merkle_batch` and `proves` is
`batch_inclusion`: the root of the batch has been written to the chain and the
membership of the unit in that batch is demonstrable. When `anchored` is
`false`, `type` is `mint_transaction` and `proves` is `token_minted`: the
transaction only proves that the token exists. It says nothing about the batch,
and nothing about the content of the passport.

`passport_anchor` dates **the document**. It carries the hash of this precise
version, its proof of membership in a tree, and the transaction that wrote the
root. It establishes a single property, `content_existed_at_or_before_tx`: this
content existed no later than that transaction. It does not make the content
true, and it does not prevent publishing a correction as the next version. The
`data_hash_matches` field at `false` means that the recorded data no longer
matches what was anchored.

The write happens on Base, chain_id `8453`.

> [!ATTENTION] Publishing does not anchor
> Anchoring is an operation that SealTrust triggers. No console screen puts it
> at your disposal, neither for an item nor for a passport version. Publishing a
> passport therefore does not anchor it, and many passports are never anchored.
> The `passport_anchor` block stays absent as long as the version has not been
> anchored, and the `anchor` block then carries `"anchored": false`. Do not
> build your integration on the presence of these blocks.

Between publication and a possible anchoring, the seal is what holds. It asks
nothing of the chain: it chains each version to the previous one and is
recomputed at read time.

For a reference passport, two blocks are absent and that absence is the right
answer: `anchor` dates an item, and a reference has none, and `verifications`
counts UID verifications, and a reference has no UID. The response additionally
carries `"level": "model"` and the GTIN.

### Checking the IPFS copy from the read

Add `verify_integrity=true` to the passport read to additionally get an
`integrity` block. The server computes that block only if it returns the IPFS
link to you, so only at the professional and authority tiers. The call must be
authenticated and carry an `access_tier`:

```bash title="curl"
curl -H "Authorization: Bearer JETON_DE_SESSION_FICTIF" \
  "https://api.sealtrust.io/v1/passport/EXEMP1E00001?access_tier=repairer&verify_integrity=true"
```

An anonymous call at the `public` or `end_user` tier never receives this block,
even with `verify_integrity=true`. For an integrity check without
authentication, use `GET /v1/passport/{identifier}/verify` described just above.

## Rate limit and errors

Every address starting with `/passport` shares a limit of 60 calls per
60-second window and per IP address. The `/passport/...` and `/v1/passport/...`
forms count against the same counter. Going over returns 429, with the
`Retry-After` header in seconds.

| Code | Condition | What to do |
| --- | --- | --- |
| 401 | professional or authority tier requested without a valid session | authenticate |
| 403 | valid session, but without rights on that tier for that brand | ask for the tier that matches your accreditation |
| 404 | `Product not found`: no product matches the identifier | check the identifier |
| 404 | `No published passport found for this product`: the product exists, no passport published | publish a version |
| 404 | `Unknown GS1 Digital Link`: no public reference passport answers for that GTIN | check the GTIN |
| 404 | no credential issued for that passport | republish the passport to trigger issuance |
| 409 | change to the content of a sealed version | publish a new version |
| 429 | rate limit exceeded | slow down |

## What to remember

The passport covers a model or an item. A batch is covered by the reference
passport of its model. A published version is sealed and no longer changes; a
correction becomes the next version, and publishing unpublishes the whole model.

The content returned depends on the tier requested, and the professional tiers
require an authenticated session together with the matching accreditation on the
brand. The three trades sit side by side: each starts from the common base, and
none receives what another adds.

The JSON-LD carries the same content, filtered the same way, with different key
names and without the headers of the JSON response.

The signed credential allows an independent verification. The seal, the IPFS
copy and the anchoring each say one precise thing, and the response names that
thing rather than leaving it to be guessed.

## Going further

- [Read a passport by identifier](/en/reference/get-passport-identifier/): the
  parameters, the full response and the errors of the read described here.
- [The proof summary](/en/reference/get-passport-proof/): the detail of the
  seal, of the IPFS copy and of the anchoring for a given item.
- [Trust and proofs](/en/confiance-et-preuves/): the exact scope of each proof
  and its limits.
- [Regulatory compliance](/en/conformite/): the obligations that bear on the
  content you publish.
- [Creating products](/en/creer-des-produits/): creation by the unit and by the
  batch, upstream of publishing a passport.
