# Publier un passeport numérique de produit

Publier un passeport, choisir qui voit quels champs, l'exporter en JSON-LD et le faire vérifier de façon indépendante.

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

---

En quittant cette page, vous saurez ce qu'un passeport SealTrust contient,
comment le publier depuis la console, comment son contenu est filtré selon la
personne qui le lit, comment le récupérer en JSON ou en JSON-LD, et comment un
tiers vérifie sa signature sans nous faire confiance.

Toutes les adresses citées ici sont publiques. La lecture d'un passeport au
niveau public ne demande aucune clef d'API.

## Sur quoi porte un passeport

Un passeport s'attache soit à un modèle de produit, soit à un exemplaire précis.

Le passeport de **référence** est rattaché au modèle. Il est partagé par tous les
articles du modèle. C'est le niveau qui convient à la plupart des produits, et il
ne demande pas de sérialiser chaque exemplaire.

Un lot se couvre par le passeport de référence de son modèle. Le règlement ESPR
autorise un passeport au niveau du modèle, du lot ou de l'article. SealTrust
rattache un passeport à un modèle ou à une unité, donc une production par lots se
couvre avec le passeport de référence du modèle : il vaut pour tous les
exemplaires qui partagent le même code produit. Vous n'avez pas à créer un
passeport par exemplaire pour produire par lots. Seules certaines batteries
doivent porter un passeport à l'exemplaire, au titre du règlement (UE) 2023/1542.

Le passeport **d'exemplaire** est rattaché à une unité physique. Il porte les
données propres à cette unité, par exemple un état de santé de batterie ou une
déclaration liée à son numéro de série.

Quand vous demandez le passeport d'une unité, le serveur cherche d'abord un
passeport rattaché à cette unité. S'il n'en trouve pas, il sert le passeport de
référence de son modèle. Il ne sert jamais à sa place un passeport rattaché à une
autre unité du même modèle.

> [!INFO] Un identifiant, trois formes
> Le champ `identifier` accepte une empreinte d'UID (`0x` suivi de 64
> caractères hexadécimaux), un identifiant de jeton (une suite de chiffres), ou
> le numéro de série imprimé sur l'étiquette (12 caractères). Le numéro de série
> est lu sans tenir compte de la casse, et les caractères ambigus sont ramenés à
> leur forme canonique : `I` et `L` valent `1`, `O` vaut `0`.

## Ce que contient un passeport

Le contenu du passeport est un document JSON libre, organisé en sections. Les
règles d'accès sont écrites contre les chemins de ces sections, ce qui fixe la
liste des noms utilisés :

| Section | Contenu |
| --- | --- |
| `product_identity` | GTIN, modèle, marque, pays de fabrication, site de production |
| `labels` | étiquettes et mentions portées sur le produit |
| `compliance` | conformité ESPR, REACH, marquage CE |
| `circularity` | recyclabilité, contenu recyclé, indice de réparabilité, notice de démontage |
| `environmental_impact` | empreinte carbone, eau, énergie, transport |
| `carbon_footprint` | empreinte carbone détaillée |
| `energy_efficiency` | classe énergétique, enregistrement EPREL |
| `durability` | durée de vie attendue |
| `materials` | composition matière |
| `substances_of_concern` | substances préoccupantes et fiches de données de sécurité |
| `bill_of_materials` | nomenclature des composants |
| `manufacturing` | données de fabrication |
| `supply_chain` | chaîne d'approvisionnement |
| `battery_specification` | spécification générale d'une batterie |
| `state_of_health` | état de santé d'une batterie |

Les deux dernières sections viennent du règlement batteries (UE) 2023/1542. Leur
présence dans un passeport suffit à le classer comme passeport de batterie pour
le choix des règles d'accès.

Autour de ces données, la lecture par unité porte aussi des éléments que le
serveur calcule lui-même : le nom du produit et de la marque, la photo du modèle,
le GTIN, le lien GS1 Digital Link, un résumé de garantie, et un bloc de
provenance des informations décrit plus bas. La lecture du passeport de référence
d'un modèle en porte moins, la liste exacte figure plus bas.

## Publier un passeport

Dans la console, ouvrez **Conformité DPP**, puis l'onglet **Passeports DPP**.
Vous y créez une version, vous la remplissez, et vous la publiez.

Quatre choses se produisent au moment où vous publiez.

**Les autres versions publiées du même modèle cessent d'être publiées.** Elles
restent enregistrées, leur date de publication est effacée et leur visibilité
passe à « marque seule ».

**Le serveur scelle la version.** Son contenu devient immuable. Le serveur
refuse toute modification du contenu d'une version scellée, avec le code 409.
Pour corriger une donnée, vous publiez une nouvelle version. Dépublier une
version ne la descelle pas.

**Le serveur dépose une copie publique sur IPFS.** Il ne dépose que la
projection publique du passeport. Les champs réservés aux niveaux professionnels
n'y figurent pas. Si le dépôt échoue, la publication aboutit quand même et le
passeport reste sans copie IPFS.

**Le serveur émet ou rafraîchit l'attestation signée.** Le passeport signé suit
automatiquement la version publiée, sans étape supplémentaire. Si la signature
échoue, la publication aboutit quand même et le passeport reste sans attestation.

> [!DANGER] Publier une unité dépublie le modèle
> Dès que le produit appartient à un modèle, le serveur rattache aussi la
> nouvelle version à ce modèle. La dépublication porte alors sur tout le modèle :
> publier un passeport rattaché à une seule unité dépublie le passeport de
> référence du modèle et les passeports de toutes les autres unités de ce modèle.
> Ces passeports repassent en visibilité « marque seule » et le point d'entrée
> `GET /v1/passport/01/{gtin}` renvoie 404. Vérifiez cette conséquence avant de
> publier un passeport d'exemplaire sur un modèle qui porte déjà un passeport de
> référence.

> [!ATTENTION] Le compte d'essai n'a pas le passeport
> Le passeport est compris dans toutes les offres payantes. Un compte d'essai n'y
> a pas droit : la création et la modification d'une version renvoient alors un
> refus portant le code `FEATURE_NOT_AVAILABLE`.

Chaque version porte un numéro qui s'incrémente. Dès que le produit appartient à
un modèle, le serveur compte ce numéro par modèle. Il ne le compte par unité que
pour un produit qui n'appartient à aucun modèle.

Trois visibilités existent : `public`, `owner_only` et `brand_only`. Le serveur
ne sert que `public` à un visiteur anonyme. Il sert en plus `owner_only` au
propriétaire actuel de l'unité quand celui-ci est authentifié. Il ne sert jamais
`brand_only` par la voie publique.

## Lire le passeport

Le point d'entrée est `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"])
```
:::

La réponse, au niveau public, a cette forme. Les valeurs sont inventées.

```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."
  }
}
```

Le domaine du champ `gs1_digital_link` est celui du résolveur configuré pour
votre intégration. `https://id.gs1.org` n'est que la valeur de repli, utilisée
quand aucun résolveur n'est configuré. Ne codez pas ce domaine en dur, lisez la
valeur renvoyée par la réponse.

Deux blocs n'apparaissent que quand ils ont lieu d'être. `lifecycle` apparaît
quand l'unité a été remplacée ou retirée : le serveur continue de servir son
passeport pour que l'identifiant résolve toujours. `integrity` apparaît quand
vous ajoutez le paramètre `verify_integrity=true` et que vous appelez à un niveau
professionnel ou autorité. Ce bloc est décrit plus bas.

### Le bloc de provenance des informations

Le bloc `evidence` dit sur quelle base chaque partie du passeport peut être crue.
Trois valeurs existent :

- `verified` : vérifié mécaniquement contre un registre public, sans déclaration.
- `document_backed` : un document tiers est joint et peut être récupéré. Son
  contenu n'a pas été audité.
- `declared` : déclaré par la marque. Enregistré, daté, attribuable, non vérifié
  de façon indépendante.

SealTrust calcule ces valeurs. Une marque ne peut pas les choisir. La section
`identity` passe à `verified` quand l'unité existe sur la chaîne. La section
`integrity` passe à `verified` quand l'empreinte de cette version a été ancrée et
correspond toujours aux données enregistrées.

### Le passeport de référence d'un modèle

Pour lire le passeport que le GTIN annonce pour le modèle, utilisez
`GET /v1/passport/01/{gtin}`.

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

La réponse a cette forme, et elle n'en a pas d'autre. Les valeurs sont inventées.

```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"
}
```

Ce point d'entrée rend moins de champs que la lecture par unité. La réponse ne
contient ni propriétaire, ni bannière de fin de vie, ni garantie : aucun de ces
trois éléments n'existe au niveau du modèle. Elle ne contient pas non plus la
photo du modèle, le lien GS1 Digital Link, le bloc de provenance des
informations, le champ `is_owner`, ni le champ `access_tier`. Si votre
intégration lit un de ces champs, lisez le passeport par unité.

Le serveur rend ici le champ `ipfs_uri` à tous les appelants, y compris
anonymes, quel que soit le niveau demandé. Traitez le contenu de cette copie IPFS
comme public. Ce point d'entrée ne rend jamais de champ `ipfs_gateway_url`.

Le paramètre `access_tier` fonctionne ici comme sur la lecture par unité : il
filtre le contenu de `data`, et les niveaux professionnels et autorité exigent la
même authentification. Le paramètre `format=jsonld` et le paramètre
`verify_integrity`, eux, n'existent pas sur ce point d'entrée.

## Niveaux d'accès, qui voit quoi

Le paramètre `access_tier` décide des champs rendus. Il accepte six valeurs.

| Niveau | Ce qu'il ajoute | Qui l'obtient |
| --- | --- | --- |
| `public` | identification, étiquettes, conformité ESPR / REACH / CE, recyclabilité et contenu recyclé, spécification générale de batterie | tout le monde, sans authentification |
| `end_user` | impact environnemental, circularité complète, matière principale, durabilité, efficacité énergétique, empreinte carbone | tout le monde, sans authentification |
| `repairer` | nomenclature, notice de démontage, indice de réparabilité, état de santé de batterie | compte authentifié accrédité réparateur sur la marque |
| `recycler` | composition matière, substances préoccupantes, notice de démontage, état de santé de batterie | compte authentifié accrédité recycleur sur la marque |
| `upstream` | composition matière, substances préoccupantes, fabrication, chaîne d'approvisionnement | compte authentifié de la marque, ou autorité |
| `authority` | l'intégralité des données | compte authentifié portant le rôle d'autorité de surveillance du marché |

> ⚠️ Ces six valeurs ne se classent pas de la plus étroite à la plus large.
> `repairer`, `recycler` et `upstream` sont trois publics distincts : chacun
> ajoute au socle du consommateur ce que son propre métier exige, et aucun ne
> contient les champs d'un autre. Un recycleur accrédité ne lit donc pas la
> nomenclature réservée au réparateur, et détenir une accréditation n'en ouvre
> aucune autre. Seul `authority` reçoit l'intégralité.

Les trois niveaux professionnels et le niveau autorité exigent une
authentification. Il s'agit d'une session de compte utilisateur, présentée soit
par l'en-tête `Authorization: Bearer <jeton de session>`, soit par le cookie de
session posé à la connexion. Une clef d'API partenaire n'ouvre pas ces niveaux.

Trois de ces niveaux correspondent à un métier : `repairer`, `recycler` et
`upstream`. Ils ne se classent pas les uns au-dessus des autres. Un recycleur
n'est pas au-dessus d'un réparateur. Chacun hérite du niveau public et du niveau
utilisateur final, puis ajoute ce que son métier demande. Les accréditations
délivrées à un partenaire sont de deux types, réparateur et recycleur, donc
aucune accréditation n'ouvre `upstream` par elle-même.

:::schema profils-acces
Un socle commun occupe le haut du schéma, servi sans authentification : le
niveau `public` porte l'identification, les étiquettes, la conformité, la
recyclabilité et la spécification générale de batterie, puis le niveau
`end_user` y ajoute l'impact environnemental, la circularité, la durabilité et
l'empreinte carbone. De ce socle partent trois flèches, vers trois cartes de
même taille placées à la même hauteur. La première, `repairer`, revient à un
compte accrédité réparateur sur la marque et ajoute la nomenclature, la notice
de démontage, l'indice de réparabilité et l'état de santé de batterie. La
deuxième, `recycler`, revient à un compte accrédité recycleur et ajoute la
composition matière, les substances préoccupantes, la notice de démontage et
l'état de santé de batterie. La troisième, `upstream`, revient à l'équipe de la
marque ou à une autorité, et ajoute la composition matière, les substances
préoccupantes, la fabrication et la chaîne d'approvisionnement. Entre chaque
paire de cartes, un signe en forme de croix rappelle qu'aucune des trois ne
reçoit ce que sa voisine ajoute. Une carte à part, en bas, porte le niveau
`authority` de la surveillance du marché, qui reçoit l'intégralité des données
quel que soit le métier.
:::

> [!ATTENTION] Un niveau de métier ne cloisonne pas un champ vis-à-vis des autres
> Ranger un champ au niveau `repairer` le rend lisible par tout partenaire
> portant l'accréditation réparateur sur votre marque, par votre équipe et par
> une autorité. Si un champ ne doit atteindre que votre équipe et une autorité,
> retirez ce champ du passeport.

Demandez le niveau de votre métier. C'est celui qui décrit ce que vous êtes, et
il rend ce que votre métier demande.

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

Les refus sont explicites :

| Code | Condition |
| --- | --- |
| 401 | niveau professionnel ou autorité demandé sans authentification |
| 403 | authentifié, mais sans l'accréditation correspondante sur cette marque, et sans accès à la marque |
| 403 | niveau `authority` demandé par un compte qui ne porte pas ce rôle |
| 404 | produit introuvable, ou aucun passeport publié pour ce produit |

Les réponses JSON portent l'en-tête `X-DPP-Access-Tier` avec le niveau
effectivement servi. Lisez-le plutôt que de supposer. L'export JSON-LD, décrit
plus bas, ne porte pas cet en-tête.

Deux comportements à connaître.

**Le propriétaire actuel voit plus.** Si l'appel porte une session valide et que
le compte est le propriétaire actuel de l'unité, le serveur répond au niveau
`end_user` à une demande au niveau `public`, et il sert en plus les passeports en
visibilité `owner_only`. Le champ `is_owner` et l'en-tête `X-DPP-Access-Tier`
signalent ce changement. Cette élévation ne vaut que pour la lecture JSON et
JSON-LD, elle ne s'applique pas à l'attestation signée.

**Aucune réponse ne se met en cache.** Le serveur sert toute réponse avec
`Cache-Control: no-store, max-age=0`, quel que soit le niveau demandé et quel
que soit le format. Un cache partagé ne rend donc jamais un corps privilégié à
l'appelant suivant. Pour réduire le nombre de vos appels, gardez le résultat
dans votre propre cache applicatif, avec la durée de fraîcheur que votre usage
tolère.

**Le lien IPFS dépend du niveau, et du point d'entrée.** Sur
`GET /v1/passport/{identifier}`, les champs `ipfs_uri` et `ipfs_gateway_url`
restent nuls aux niveaux `public` et `end_user`. Le serveur ne les renseigne
qu'aux niveaux professionnels et autorité, qui sont authentifiés. Sur
`GET /v1/passport/01/{gtin}`, le serveur rend `ipfs_uri` à tous les appelants,
y compris anonymes.

## Définir vos propres règles d'accès

Par défaut, la répartition des champs entre niveaux est celle décrite ci-dessus.
Vous pouvez la remplacer, marque par marque, dans **Conformité DPP** puis
**Règles d'accès**. Une règle associe un chemin de champ à un niveau. Les
chemins acceptent le caractère générique, par exemple `materials.*`.

Les règles peuvent être portées par groupe de produits. Les règles du groupe
`general` s'appliquent partout ; les règles du groupe `battery` s'appliquent en
plus aux passeports de batterie.

> [!DANGER] Une seule règle remplace tous les défauts
> Dès que votre marque possède au moins une règle, les règles intégrées ne sont
> plus utilisées du tout pour cette marque. Un champ qu'aucune de vos règles ne
> couvre n'est plus rendu à personne, sauf au niveau `authority`. Avant d'écrire
> votre première règle, utilisez l'action qui recopie les défauts sous forme de
> règles, puis modifiez ce qui doit l'être. Cette action n'écrase rien, elle
> ajoute seulement ce qui manque.

La suppression de toutes les règles d'une marque en une fois demande de retaper
une phrase de confirmation. Cette phrase contient le nombre exact de règles qui
seraient supprimées et la portée de l'opération. Elle vous est fournie par
l'écran qui liste les règles. Sans elle, la suppression est refusée.

Cette confirmation existe parce que l'opération est un changement de
confidentialité sur des données réglementées. Les champs que vous aviez
restreints reviennent aux règles intégrées, ce qui peut les rendre lisibles
publiquement.

## Export JSON-LD

Ajoutez `format=jsonld` pour obtenir le passeport sous forme de données liées,
en vocabulaire Schema.org et GS1. Le serveur rend cette réponse avec le type de
contenu `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
  }
}
```

Quatre points à retenir.

L'export JSON-LD est construit **après** le filtrage par niveau. Un appel anonyme
obtient donc la projection publique en JSON-LD, et rien de plus. Pour obtenir les
champs professionnels en JSON-LD, ajoutez `access_tier` et authentifiez-vous.

Les noms de clés changent entre les deux formats. La section `compliance` du JSON
devient un nœud `espr:compliance` de type `espr:ComplianceDeclaration`, dont les
clés sont `espr:euEsprCompliant`, `espr:reachCompliant`, `espr:ceMarking` et
leurs voisines. N'attendez pas les noms de la réponse JSON dans le JSON-LD.

Le serveur retire du document les clés sans valeur. Un champ que vous n'avez pas
rempli n'apparaît pas sous forme de `null`.

Cette réponse ne porte pas l'en-tête `X-DPP-Access-Tier`, contrairement à la
réponse JSON. Le niveau servi est celui que vous avez demandé dans
`access_tier`.

Le paramètre `format` n'existe que sur `GET /v1/passport/{identifier}`. Le point
d'entrée du passeport de référence d'un GTIN ne le propose pas.

## Le passeport signé et sa vérification

Le serveur émet aussi chaque passeport publié sous forme d'attestation
vérifiable, au format SD-JWT-VC. Cette attestation permet à un tiers de vérifier
que la marque a bien émis ce passeport, sans passer par notre point d'entrée de
vérification et sans nous accorder de confiance.

### L'émetteur

L'émetteur est un identifiant décentralisé `did:web` porté par la marque. Sa
forme par défaut est `did:web:api.sealtrust.io:brand:{brand_id}`, qui se résout,
selon la spécification did:web, à l'adresse
`https://api.sealtrust.io/brand/{brand_id}/did.json`.

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

Le document rendu liste toutes les clefs publiques non révoquées de la marque,
chacune sous forme de `JsonWebKey2020`. L'identifiant d'une clef est
`<did>#key-<version>`, ce qui permet à une attestation ancienne de rester
vérifiable après une rotation de clef, tant que l'ancienne version n'est pas
révoquée.

Une marque peut aussi porter son identifiant sur son propre domaine. Le document
se trouve alors à `https://<domaine de la marque>/.well-known/did.json`, et
l'identifiant prend la forme `did:web:<domaine de la marque>`. La marque conserve
ainsi la propriété de son identité d'émetteur.

> [!INFO] Où vit la clef privée
> La clef privée de signature n'est jamais rendue par l'API. Nous ne publions
> que la partie publique, celle que porte le document de l'émetteur, et c'est
> elle qui vous sert à vérifier.

### Récupérer l'attestation

`GET /v1/passport/{identifier}/vc` rend la présentation filtrée selon le niveau
demandé. Cette voie applique la même grille de niveaux que la lecture JSON, avec
deux différences :

- l'élévation automatique du propriétaire ne s'applique pas. Un propriétaire
  authentifié reste au niveau qu'il demande, et le serveur ne sert pas les
  passeports en visibilité `owner_only` ;
- une unité remplacée ou retirée renvoie 404, alors que la lecture JSON continue
  de servir son passeport avec la bannière `lifecycle`.

Ces deux différences valent pour les trois adresses de l'attestation : `/vc`,
`/vc/verify` et `/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~"
}
```

La divulgation est sélective. Le serveur laisse en clair les champs du niveau
public et remplace les autres par des empreintes. Il ne révèle une empreinte que
si le niveau demandé y donne droit.

Deux blocs échappent à cette règle. Le serveur les porte toujours en clair, quel
que soit le niveau demandé, et il ne les masque jamais :

- l'identité de la marque : raison sociale, identifiant LEI, numéro EORI, site,
  adresse postale et courriel de contact ;
- l'identité de l'unité : empreinte d'UID, identifiant de jeton et nom du
  produit.

Une présentation au niveau public révèle donc la projection publique, plus ces
deux blocs. La lecture JSON au niveau public, elle, ne rend de la marque que son
nom. Vérifiez le contenu de la fiche de votre marque avant de diffuser des
attestations : son adresse postale et son courriel de contact partent avec.

Si aucune attestation n'a encore été émise pour ce passeport, la réponse est 404.

### Vérifier l'attestation

Deux chemins existent, et le second ne dépend pas de nous.

**Par notre point d'entrée.** `GET /v1/passport/{identifier}/vc/verify` vérifie
la présentation correspondant au niveau demandé contre la clef publique de la
marque.

:::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"
    }
  }
}
```

En cas d'échec, `verified` vaut `false`, `error` vaut `verification_failed` et
`credential_subject` est nul. Le message d'erreur d'origine n'est jamais rendu.

**Par vos propres moyens.** Récupérez la présentation avec
`GET /v1/passport/{identifier}/vc`, récupérez le document de l'émetteur à
l'adresse `did.json` indiquée plus haut, choisissez la clef dont l'identifiant
correspond au champ `kid` de l'en-tête de l'attestation, et vérifiez la
signature. L'algorithme de signature est `ES256`, et le type déclaré dans
l'en-tête est `dc+sd-jwt`. N'importe quelle bibliothèque did:web et SD-JWT-VC
standard suffit.

### Voir le contenu avant signature

`GET /v1/passport/{identifier}/vc/preview` rend l'enveloppe de l'attestation et
son contenu pour un niveau donné, sans signature. La réponse porte
`"signed": false`.

L'aperçu rend le contenu du niveau demandé au format JSON-LD. L'attestation
signée, elle, porte l'arbre de données brut du passeport, avec les noms de
sections de la réponse JSON. Servez-vous de l'aperçu pour vérifier quels champs
un niveau donné voit. Sa structure n'est pas celle de l'attestation, donc ne
l'utilisez pas comme gabarit d'intégration.

## Preuves d'intégrité

Deux points d'entrée publics accompagnent le passeport. Ils sont sans
authentification.

### Le contrôle d'intégrité

`GET /v1/passport/{identifier}/verify` recalcule l'empreinte des données, la
compare à celle enregistrée, compare la copie IPFS quand elle existe, et
recalcule l'empreinte de version d'une version scellée.

```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
  }
}
```

Soyez précis sur ce que chaque ligne établit.

`db_hash_match` compare une donnée que nous détenons à une empreinte que nous
détenons. C'est un contrôle de cohérence interne.

`ipfs_match` compare la copie publique déposée sur IPFS à la projection publique
attendue. La copie est adressée par son empreinte, donc elle ne peut pas être
modifiée sans changer d'adresse. Quand la copie ne peut pas être récupérée, la
valeur est `null`, ce qui veut dire « inconnu ». Elle ne bascule jamais à `false`
pour une simple panne de passerelle.

`version_hash` porte l'empreinte de cette version, 64 caractères hexadécimaux.
`prev_version_hash` porte l'empreinte de la version précédente, et il vaut `null`
pour la première version scellée. Chaque empreinte de version couvre celle qui la
précède, donc réécrire une version détache toutes celles publiées après elle.

`chain_link_match` est le signal de falsification. Si `chain_link_match` vaut
`false`, la version a été modifiée après sa publication. Un passeport publié
avant l'existence de ce chaînage porte `linked: false` et
`reason: sealed_before_chain`, et aucune empreinte de version n'est fabriquée
après coup pour une publication que nous ne pouvons pas dater.

Le serveur ne rend le lien IPFS ici que si la copie déposée est prouvée identique
à la projection publique. Dans le cas contraire, il retire le lien de la réponse.

### Le résumé des preuves

`GET /v1/passport/{identifier}/proof` rassemble tout ce qu'un tiers peut
vérifier. Le même résumé existe pour un passeport de référence à
`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"
  }
}
```

Deux blocs portent une référence sur la chaîne, et ils ne disent pas la même
chose.

`anchor` date **l'article**. Lisez le champ `anchored`. Le nom de la clef ne
suffit pas.
Quand `anchored` vaut `true`, `type` vaut `merkle_batch` et `proves` vaut
`batch_inclusion` : la racine du lot a été inscrite sur la chaîne et
l'appartenance de l'unité à ce lot est démontrable. Quand `anchored` vaut
`false`, `type` vaut `mint_transaction` et `proves` vaut `token_minted` : la
transaction prouve seulement que le jeton existe. Elle ne dit rien du lot, et
rien du contenu du passeport.

`passport_anchor` date **le document**. Il porte l'empreinte de cette version
précise, sa preuve d'appartenance à un arbre, et la transaction qui a inscrit la
racine. Il établit une seule propriété, `content_existed_at_or_before_tx` : ce
contenu existait au plus tard à cette transaction. Il ne rend pas le contenu
vrai, et il n'empêche pas de publier une correction en version suivante. Le
champ `data_hash_matches` à `false` signifie que les données enregistrées ne
correspondent plus à ce qui a été ancré.

L'inscription se fait sur Base, chain_id `8453`.

> [!ATTENTION] Publier n'ancre pas
> L'ancrage est une opération que SealTrust déclenche. Aucun écran de la console
> ne la met à votre disposition, ni pour un article ni pour une version de
> passeport. Publier un passeport ne l'ancre donc pas, et beaucoup de passeports
> ne sont jamais ancrés. Le bloc `passport_anchor` reste absent tant que
> la version n'a pas été ancrée, et le bloc `anchor` porte alors
> `"anchored": false`. Ne construisez pas votre intégration sur la présence de
> ces blocs.

Entre la publication et un éventuel ancrage, c'est le sceau qui tient. Il ne
demande rien à la chaîne : il chaîne chaque version à la précédente et se
recalcule à la lecture.

Pour un passeport de référence, deux blocs sont absents et cette absence est la
réponse juste : `anchor` date un article, or une référence n'en a pas, et
`verifications` compte des vérifications d'UID, or une référence n'a pas d'UID.
La réponse porte en plus `"level": "model"` et le GTIN.

### Vérifier la copie IPFS depuis la lecture

Ajoutez `verify_integrity=true` à la lecture du passeport pour obtenir en plus un
bloc `integrity`. Le serveur ne calcule ce bloc que s'il vous rend le lien IPFS,
donc seulement aux niveaux professionnels et autorité. L'appel doit être
authentifié et porter un `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"
```

Un appel anonyme au niveau `public` ou `end_user` ne reçoit jamais ce bloc, même
avec `verify_integrity=true`. Pour un contrôle d'intégrité sans authentification,
utilisez `GET /v1/passport/{identifier}/verify` décrit juste au-dessus.

## Plafond d'appels et erreurs

Toutes les adresses commençant par `/passport` partagent un plafond de 60 appels
par tranche de 60 secondes et par adresse IP. Les formes `/passport/...` et
`/v1/passport/...` comptent sur le même compteur. Un dépassement renvoie 429,
avec l'en-tête `Retry-After` en secondes.

| Code | Condition | Que faire |
| --- | --- | --- |
| 401 | niveau professionnel ou autorité demandé sans session valide | authentifiez-vous |
| 403 | session valide, mais sans droit sur ce niveau pour cette marque | demandez le niveau qui correspond à votre accréditation |
| 404 | `Product not found` : aucun produit ne correspond à l'identifiant | vérifiez l'identifiant |
| 404 | `No published passport found for this product` : le produit existe, aucun passeport publié | publiez une version |
| 404 | `Unknown GS1 Digital Link` : aucun passeport de référence public ne répond pour ce GTIN | vérifiez le GTIN |
| 404 | aucune attestation émise pour ce passeport | republiez le passeport pour déclencher l'émission |
| 409 | modification du contenu d'une version scellée | publiez une nouvelle version |
| 429 | plafond d'appels dépassé | ralentissez |

## Ce qu'il faut retenir

Le passeport porte sur un modèle ou sur un exemplaire. Un lot se couvre par le
passeport de référence de son modèle. Une version publiée est scellée et ne se
modifie plus ; une correction devient la version suivante, et publier dépublie
tout le modèle.

Le contenu rendu dépend du niveau demandé, et les niveaux professionnels exigent
une session authentifiée accompagnée de l'accréditation correspondante sur la
marque. Les trois métiers sont côte à côte : chacun part du socle commun, et
aucun ne reçoit ce qu'un autre ajoute.

Le JSON-LD porte le même contenu, filtré de la même façon, avec d'autres noms de
clés et sans les en-têtes de la réponse JSON.

L'attestation signée permet une vérification indépendante. Le sceau, la copie
IPFS et l'ancrage disent chacun une chose précise, et la réponse nomme cette
chose plutôt que de la laisser deviner.

## Pour aller plus loin

- [Lire un passeport par identifiant](/reference/get-passport-identifier/) : les
  paramètres, la réponse complète et les erreurs de la lecture décrite ici.
- [Le résumé des preuves](/reference/get-passport-proof/) : le détail du sceau,
  de la copie IPFS et de l'ancrage pour un article donné.
- [Confiance et preuves](/confiance-et-preuves/) : la portée exacte de chaque
  preuve et ses limites.
- [Conformité réglementaire](/conformite/) : les obligations qui portent sur le
  contenu que vous publiez.
- [Créer des produits](/creer-des-produits/) : la création à l'unité et en lot,
  en amont de la publication d'un passeport.
