# GET /passport/{identifier}/vc/verify

Contrôler la signature du passeport numérique délivré sous forme d'attestation vérifiable, et lire les données révélées au niveau d'accès demandé. Aucune session n'est demandée pour les niveaux public et end_user.

Source : https://docs.sealtrust.io/reference/get-passport-vc-verify/

---

Vous faites contrôler la signature du passeport numérique d'un produit, et vous
récupérez les données que cette signature couvre. En quittant cette page, vous
saurez demander ce contrôle depuis n'importe quel identifiant de produit,
distinguer un contrôle qui échoue d'une requête qui échoue, et savoir quelles
données la réponse vous montre selon le niveau d'accès que vous demandez.

Adresse complète :

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

Le même point d'entrée répond aussi sans le préfixe `/v1`, à
`https://api.sealtrust.io/passport/{identifier}/vc/verify`. Les deux adresses
appellent le même code. Utilisez la forme `/v1` pour une nouvelle intégration.

Le passeport est délivré au format SD-JWT-VC, une attestation signée dont
chaque champ peut être révélé ou retenu séparément. L'émetteur est la marque,
identifiée par un identifiant décentralisé `did:web`. Ce point d'entrée
recompose l'attestation au niveau d'accès que vous demandez, contrôle sa
signature, et vous rend les données révélées.

> [!INFO] Le contrôle porte sur la présentation filtrée
> Nous réduisons d'abord l'attestation au niveau d'accès que vous demandez,
> puis nous contrôlons sa signature. Ce point d'entrée ne peut donc jamais
> montrer plus de champs que la lecture ordinaire du passeport au même niveau.

## Autorisation

Aucun droit de clef d'API n'est vérifié sur ce point d'entrée. Deux contrôles
s'appliquent malgré tout : l'origine de votre appel, puis le niveau d'accès que
vous demandez.

### D'où vous appelez

Appelez ce point d'entrée depuis votre serveur.

Nous refusons en 403 tout appel qui porte un en-tête `Origin` ou `Referer`
désignant un domaine autre que les nôtres. Le champ `detail` vaut alors
`Forbidden origin`. Un navigateur pose toujours l'un de ces deux en-têtes, donc
une page web hébergée ailleurs que chez nous ne peut pas appeler cette adresse
depuis le navigateur de son visiteur.

Le cookie de session ne fonctionne que depuis une page servie par un de nos
domaines. Nous refusons en 403 un appel qui porte ce cookie sans en-tête
`Origin` ni `Referer`, avec `detail` à `Origin or Referer header required`.
Depuis un serveur, présentez donc le jeton de session dans l'en-tête
`Authorization: Bearer`.

### Le niveau que vous demandez

Les niveaux `public` et `end_user` ne demandent aucun compte. Les cinq autres
valeurs du paramètre `access_tier` exigent une session de compte, présentée par
l'en-tête `Authorization: Bearer <jeton de session>` ou par le cookie de
session posé à la connexion.

| Niveau demandé | Ce qu'il faut |
| --- | --- |
| `public` | rien |
| `end_user` | rien |
| `repairer` | une session, et une accréditation de réparateur active sur la marque du produit |
| `recycler` | une session, et une accréditation de recycleur active sur la marque du produit |
| `upstream` | une session ayant accès à la marque du produit, ou le rôle d'autorité |
| `authority` | une session portant le rôle d'autorité de surveillance du marché |

Une session ayant accès à la marque du produit ouvre les trois niveaux
de métier sur ses propres produits. Le rôle d'autorité de surveillance du
marché les ouvre également, sur tous les produits.

Une clef d'API partenaire n'ouvre rien ici. Le jeton reçu dans l'en-tête
`Authorization` est décodé comme un jeton de session de compte utilisateur, et
une clef d'API n'en est pas un. La lecture échoue en silence et l'appel se
poursuit comme un appel anonyme.

## Plafond d'appels

60 appels par tranche de 60 secondes, comptés par adresse IP appelante. La
fenêtre est fixe.

Ce plafond est partagé par toutes les adresses qui commencent par `/passport`.
Les formes `/passport/…` et `/v1/passport/…` alimentent le même compteur, le
préfixe `/v1` ne crée pas un second budget.

Chaque réponse acceptée porte trois en-têtes qui décrivent ce compteur.

| En-tête | Contenu |
| --- | --- |
| `X-RateLimit-Limit` | le plafond appliqué sur la fenêtre, ici `60` |
| `X-RateLimit-Remaining` | ce qu'il vous reste dans la fenêtre en cours |
| `X-RateLimit-Reset` | l'horodatage de fin de la fenêtre, en secondes |

Un dépassement renvoie 429, avec les mêmes trois en-têtes et un `Retry-After`.
Sur ce point d'entrée, `Retry-After` vaut la durée de la fenêtre, soit 60
secondes.

> [!INFO] Cet appel ne consomme aucun quota
> Le quota quotidien d'une clef d'API n'est pas entamé par cet appel, et le
> quota mensuel de produits de votre offre non plus. Ce point d'entrée
> n'interroge ni l'un ni l'autre.

## Paramètres de chemin et de requête

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `identifier` | `string` | oui | L'identifiant du produit. Trois formes sont acceptées, voir ci-dessous. |
| `access_tier` | `string` | non | Le niveau d'accès demandé. Vaut `public` par défaut. Six valeurs acceptées, listées plus bas. Nous refusons toute autre valeur en 422. |

### Les trois formes d'identifiant acceptées

| Forme | À quoi elle ressemble | Où vous la trouvez |
| --- | --- | --- |
| Numéro de série | 12 caractères, chiffres et lettres | Imprimé sur le produit, c'est ce que porte son QR |
| Identifiant de jeton | Une suite de chiffres, souvent très longue | Rendu par nos réponses dans le champ `token_id` |
| Empreinte d'identifiant | `0x` suivi de 64 caractères hexadécimaux | Rendue par nos réponses dans le champ `uid_hash` |

L'empreinte d'identifiant existe pour un produit en QR seul comme pour un
produit à puce NFC. Le serveur la tire au hasard pour un produit en QR seul. Il
la dérive de l'identifiant de la puce pour un produit à puce NFC. Les deux
formes ont donc la même allure et se demandent de la même manière.

Nous reconnaissons la forme à l'écriture. Une valeur qui commence par `0x` et
fait exactement 66 caractères, nous la cherchons comme une empreinte
d'identifiant. Toute autre valeur, nous la cherchons d'abord comme un
identifiant de jeton, puis comme un numéro de série quand la première recherche
n'a rien donné.

Vous écrivez l'empreinte d'identifiant dans la casse que vous voulez. Vous
écrivez le numéro de série dans la casse que vous voulez également, et nous le
canonicalisons comme le fait le résolveur du QR : nous y lisons les lettres `I`
et `L` comme un `1`, la lettre `O` comme un `0`. Vous pouvez donc recopier à la
main un numéro lu sur une étiquette.

Le numéro de certificat d'authenticité n'est pas accepté ici.

Un produit détruit sur la chaîne ou retiré du catalogue ne se résout plus par
ce point d'entrée, et la réponse est alors 404.

### Les six valeurs de `access_tier`

Ces niveaux sont des publics différents, sans hiérarchie entre eux. Chacun des
trois niveaux de métier hérite du niveau public et du niveau utilisateur
final, puis ajoute ce que son métier demande.

| Valeur | Ce qu'elle ajoute aux champs révélés |
| --- | --- |
| `public` | identification du produit, conformité ESPR, REACH et marquage CE, pourcentage de recyclabilité, pourcentage de contenu recyclé, étiquettes, spécification générale de batterie |
| `end_user` | impact environnemental, circularité complète, matière principale, matière certifiée biologique, durabilité, efficacité énergétique, empreinte carbone |
| `repairer` | nomenclature, notice de démontage, indice de réparabilité, état de santé de batterie |
| `recycler` | composition matière, substances préoccupantes, notice de démontage, état de santé de batterie |
| `upstream` | composition matière, substances préoccupantes, fabrication, chaîne d'approvisionnement |
| `authority` | l'intégralité des données, sans filtrage |

Une marque peut remplacer ces règles par les siennes. Le tableau ci-dessus
décrit ce qui s'applique à défaut de règles propres à la marque.

### En-têtes

Aucun en-tête n'est requis pour les niveaux `public` et `end_user`. Les cinq
autres niveaux exigent l'en-tête `Authorization` ou le cookie de session. Dans
tous les cas, respectez la règle d'origine décrite plus haut.

## Corps de la requête

Aucun. Cette requête n'a pas de corps.

## Requête d'exemple

Contrôle de l'attestation du produit dont le numéro imprimé est
`EXEMPLE00001`, au niveau public.

Les trois exemples s'exécutent depuis un serveur. Aucun ne fonctionne dans le
navigateur d'un visiteur : le navigateur pose un en-tête `Origin` que nous
refusons, et vous recevez 403.

> [!INFO] Le SDK TypeScript ne couvre pas ce point d'entrée
> Aucune méthode du paquet `@sealtrust-io/sdk` en version 0.3.0 n'appelle cette
> adresse. L'exemple TypeScript ci-dessous utilise `fetch`, sans dépendance, et
> s'exécute sous Node. Le serveur MCP, lui, expose ce contrôle sous le nom
> d'outil `verify_credential`.

:::onglets
```bash title="curl"
curl -i "https://api.sealtrust.io/v1/passport/EXEMPLE00001/vc/verify?access_tier=public"
```
```typescript
const url = new URL(
  "https://api.sealtrust.io/v1/passport/EXEMPLE00001/vc/verify",
);
url.searchParams.set("access_tier", "public");

const reponse = await fetch(url);

if (reponse.status === 404) {
  console.log("Aucune attestation signée à contrôler pour ce produit.");
} else if (reponse.status === 403) {
  console.log(
    "Appel refusé : exécutez ce code depuis un serveur, jamais depuis un navigateur.",
  );
} else if (reponse.ok) {
  const resultat = await reponse.json();
  console.log(resultat.verified, resultat.error);
  console.log(resultat.issuer, resultat.key_version);
  console.log(resultat.credential_subject);
} else {
  console.log(reponse.status, await reponse.json());
}
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/passport/EXEMPLE00001/vc/verify",
    params={"access_tier": "public"},
    timeout=30,
)

if response.status_code == 404:
    print("Aucune attestation signée à contrôler pour ce produit.")
elif response.status_code == 403:
    print("Appel refusé : exécutez ce code depuis un serveur, jamais depuis un navigateur.")
elif response.ok:
    resultat = response.json()
    print(resultat["verified"], resultat["error"])
    print(resultat["issuer"], resultat["key_version"])
    print(resultat["credential_subject"])
else:
    print(response.status_code, response.json())
```
:::

## Réponse d'exemple

Code HTTP `200`. La signature est valide et le niveau demandé est `public`.

```json
{
  "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"
    },
    "compliance": {
      "eu_espr": true,
      "reach": true,
      "ce_marking": false
    },
    "circularity": {
      "recyclability_percentage": 62,
      "recycled_content_percentage": 0
    }
  }
}
```

La réponse compte huit champs et rien d'autre.

| Champ | Type | Description |
| --- | --- | --- |
| `passport_id` | `integer` | L'identifiant de la version de passeport sur laquelle le contrôle a porté. |
| `issuer` | `string` | L'identifiant décentralisé `did:web` de la marque émettrice. Toujours renseigné sur ce point d'entrée. |
| `vct` | `string` | Le type d'attestation. Vaut `https://schema.sealtrust.io/vct/digital-product-passport` quand la marque n'en a pas déclaré un autre. |
| `key_version` | `integer` ou `null` | Le numéro de version de la clef de signature de la marque qui a signé l'attestation. `null` quand ce numéro n'a pas été enregistré à la délivrance. |
| `access_tier` | `string` | Le niveau d'accès demandé, repris tel quel. Ce point d'entrée ne change jamais le niveau demandé. |
| `verified` | `boolean` | `true` quand la signature a été contrôlée avec succès. |
| `error` | `string` ou `null` | `null` quand `verified` vaut `true`. Vaut `verification_failed` sinon. C'est la seule valeur possible. |
| `credential_subject` | `object` ou `null` | Les données révélées au niveau demandé, telles que la signature les couvre. Vaut `null` dès que `verified` vaut `false`. |

### Un contrôle qui échoue reste une réponse 200

C'est le point à retenir de cette page. Un échec de contrôle n'est pas une
erreur HTTP. La réponse reste 200 et porte le verdict.

```json
{
  "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": false,
  "error": "verification_failed",
  "credential_subject": null
}
```

Lisez donc toujours `verified`. Un code 200 ne dit rien à lui seul.

### Ce qui fait basculer `verified` à `false`

| Cause | Ce qu'elle signifie |
| --- | --- |
| La signature ne correspond pas au contenu | L'attestation a été modifiée après sa délivrance. |
| L'émetteur inscrit dans l'attestation n'est pas celui attendu pour cette marque | L'attestation a été délivrée sous une autre identité que celle de la marque du produit. |
| L'attestation ne désigne aucune version de clef lisible | L'en-tête de l'attestation ne porte pas de numéro de version de clef exploitable. |
| La version de clef citée par l'attestation n'existe pas pour cette marque | La clef qui a signé n'est pas connue. |
| Cette version de clef a été révoquée | La marque a retiré cette clef. Les attestations qu'elle a signées ne sont plus reconnues. |

La réponse ne dit pas laquelle de ces causes s'applique. Le champ `error` vaut
`verification_failed` dans tous ces cas.

### Contrôler la signature vous-même

Vous n'êtes pas obligé de nous demander ce verdict. La marque publie ses clefs
publiques de signature dans un document d'identité décentralisé, servi
publiquement à l'adresse `GET https://api.sealtrust.io/brand/{brand_id}/did.json`.
Une marque qui héberge son identité sur son propre domaine le sert à l'adresse
`https://<son domaine>/.well-known/did.json`.

Avec ce document et n'importe quelle bibliothèque `did:web` et SD-JWT-VC du
commerce, vous contrôlez la signature sans passer par nous. C'est ce qui rend
le passeport opposable sans dépendre de notre disponibilité.

## Erreurs

Le corps d'une réponse d'erreur porte un champ `detail`.

Les contrôles s'enchaînent dans cet ordre : origine de l'appel, résolution du
produit, recherche du passeport publié, contrôle du niveau demandé, présence
d'une attestation délivrée, puis identification de la marque émettrice. La
première étape qui échoue donne la réponse.

| Code | Condition | Que faire |
| --- | --- | --- |
| 401 | `access_tier=authority` est demandé sans session valide. `detail` vaut `Authority-tier access requires authentication`. | Connectez-vous avec un compte portant le rôle d'autorité de surveillance du marché. Une clef d'API partenaire ne convient pas. |
| 401 | `access_tier` vaut `repairer`, `recycler` ou `upstream`, et l'appel ne porte aucune session valide. `detail` vaut `Professional-tier access requires authentication`. | Présentez un jeton de session de compte, ou demandez le niveau `public` ou `end_user`. |
| 403 | L'appel porte un en-tête `Origin` ou `Referer` qui ne désigne pas un de nos domaines, ce qui arrive pour tout appel émis depuis une page web hébergée ailleurs. `detail` vaut `Forbidden origin`. | Appelez ce point d'entrée depuis votre serveur. Un appel émis par le navigateur d'un visiteur ne peut aboutir. |
| 403 | L'appel porte le cookie de session et n'a ni en-tête `Origin` ni en-tête `Referer`, ce qui arrive quand on rejoue un cookie de navigateur en ligne de commande. `detail` vaut `Origin or Referer header required`. | Retirez le cookie et présentez le jeton de session dans l'en-tête `Authorization: Bearer`. |
| 403 | `access_tier=authority` est demandé par un compte connecté qui ne porte pas ce rôle. `detail` vaut `Authority-tier access is restricted to market surveillance authorities`. | Demandez le niveau qui correspond à votre habilitation. |
| 403 | Un niveau professionnel est demandé par un compte connecté qui n'a ni accès à la marque du produit, ni l'accréditation correspondante sur cette marque. `detail` vaut `This tier is restricted to the product's brand, partners holding the matching accreditation, and market-surveillance authorities`. | Demandez à la marque l'accréditation qui correspond à votre métier, puis demandez le niveau de ce métier. |
| 404 | Aucun produit ne correspond à cet identifiant, sous aucune des trois formes acceptées. `detail` vaut `Product not found`. | Vérifiez l'identifiant. Un produit détruit sur la chaîne ou retiré du catalogue donne cette même réponse. |
| 404 | Le produit existe, mais aucun passeport public n'est publié pour lui ni pour son modèle. `detail` vaut `No published passport found for this product`. | Il n'y a rien à contrôler. Un passeport réservé au propriétaire ou à la marque donne aussi cette réponse. |
| 404 | Un passeport public existe, mais aucune attestation signée n'a été délivrée pour cette version. `detail` vaut `No VC issued for this passport yet`. | Demandez à la marque de délivrer l'attestation de cette version du passeport. Le passeport reste lisible par les points d'entrée de lecture. |
| 404 | La marque du passeport n'a pas pu être retrouvée. `detail` vaut `Brand not found`. | Contactez le support en indiquant l'identifiant que vous avez appelé. Aucune action de votre côté ne corrige cette réponse. |
| 422 | `access_tier` n'est pas une des six valeurs acceptées. `detail` est une liste, chaque entrée portant `loc`, `type` et `msg`. | Lisez `loc` pour savoir quel paramètre est en cause, puis corrigez sa valeur. |
| 429 | Le plafond de 60 appels par 60 secondes est atteint pour votre adresse IP, tous chemins commençant par `/passport` confondus. `detail` vaut `Rate limit exceeded: 60 requests per 60s`. | Attendez le nombre de secondes indiqué par `Retry-After`, puis réessayez. |
| 500 | Une erreur inattendue s'est produite pendant le traitement de votre appel. `detail` vaut `Internal Server Error`. La réponse porte un en-tête `X-Request-Id`. | Réessayez. Si l'erreur persiste, contactez le support en indiquant la valeur de `X-Request-Id`. |

> [!ATTENTION] Aucun code d'erreur ne signale une signature invalide
> Une attestation dont la signature ne tient pas répond 200 avec `verified` à
> `false`. Un traitement qui se contente de regarder le code HTTP accepterait
> donc un passeport dont la signature ne vaut rien.

## Voir aussi

- [`GET /passport/{identifier}/vc`](/reference/get-passport-vc/),
  récupérer le justificatif signé du passeport, au format SD-JWT-VC.
- [`GET /passport/{identifier}/vc/preview`](/reference/get-passport-vc-preview/),
  voir, sans signature, ce qu'un niveau d'accès exposerait.
- [`GET /brand/{brand_id}/did.json`](/reference/get-brand-did-json/),
  récupérer les clefs publiques de signature d'une marque.
- [`GET /.well-known/did.json`](/reference/get-well-known-did-json/),
  servir le document d'identité d'une marque sur son propre domaine.
