# GET /resolve/{identifier}

Lire en un seul appel tout ce qu'une page produit affiche : identité, certificat, passeport public, médias, historique et preuves d'ancrage. Point d'entrée public, sans clef d'API.

Source : https://docs.sealtrust.io/reference/get-resolve/

---

Vous obtenez en un seul appel tout ce qu'une page produit affiche : l'identité
de l'article, son certificat en cours, son passeport publié, ses médias, son
historique et ses preuves d'ancrage sur la chaîne. Aucune clef d'API n'est
demandée.

L'adresse complète est `https://api.sealtrust.io/v1/resolve/{identifier}`. La
même route existe sans le préfixe `/v1`, et c'est la forme `/v1` qui est
recommandée pour une nouvelle intégration.

Un seul paramètre suffit : l'identifiant de l'article. Quatre formes sont
acceptées, et vous n'avez pas à déclarer laquelle vous envoyez. Le serveur les
essaie dans l'ordre.

> [!INFO] Cette réponse est publique
> Ce point d'entrée sert les pages que lisent vos clients finaux. Le serveur
> filtre les données du passeport au niveau consommateur. Avec le découpage
> par défaut, les sections réservées aux professionnels n'y figurent pas :
> nomenclature de fabrication, composition matière au-delà des matières
> principales, substances préoccupantes, données de fabrication et chaîne
> d'approvisionnement. Une marque qui définit ses propres règles d'accès
> remplace ce découpage par le sien, y compris pour le niveau public. Un
> professionnel accrédité lit ces sections par un autre chemin, authentifié.

## Autorisation

Aucune, point d'entrée public. Il n'attend ni clef d'API, ni cookie de session,
ni en-tête `Authorization`. Un appel serveur à serveur est accepté.

## Plafond d'appels

60 appels par fenêtre de 60 secondes, comptés par adresse IP appelante. Le
plafond est partagé par toutes les adresses qui commencent par `/resolve`, et
il s'applique aussi bien à `/resolve/{identifier}` qu'à
`/v1/resolve/{identifier}`. Le compteur est commun à toutes les valeurs
d'identifiant : parcourir mille identifiants différents consomme mille appels
du même budget.

Les réponses 200, 404, 405 et 429 portent les en-têtes `X-RateLimit-Limit`,
`X-RateLimit-Remaining` et `X-RateLimit-Reset`, ce dernier donnant l'heure de
remise à zéro en secondes depuis le 1er janvier 1970. Une réponse 500 ne les
porte pas. Lisez-les toujours avec une valeur de repli. Un dépassement renvoie
429 avec en plus `Retry-After`, en secondes.

Ce point d'entrée ne consomme aucun quota de votre offre.

Toute réponse porte l'en-tête `Cache-Control: no-store, max-age=0`. Ne mettez
cette réponse dans aucun cache partagé. 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.

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

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `identifier` | `string` | oui | L'identifiant de l'article. Quatre formes acceptées, décrites ci-dessous. Le serveur retire les espaces de bord avant la recherche. |

Ce point d'entrée n'a aucun paramètre de requête.

### Les quatre formes d'identifiant

Le serveur les essaie dans cet ordre et s'arrête à la première qui trouve un
article.

| Ordre | Forme | Reconnue à | Sensible à la casse |
| --- | --- | --- | --- |
| 1 | Empreinte de l'article | `0x` suivi de 64 caractères hexadécimaux, soit 66 caractères | non |
| 2 | Identifiant du jeton | Toute valeur, comparée telle quelle à l'identifiant de jeton enregistré | oui |
| 3 | Numéro de série imprimé | 12 caractères de l'alphabet Crockford Base32, qui exclut les lettres I, L, O et U | non |
| 4 | Numéro de certificat | La valeur exacte du champ `certificate_number`, par exemple `ST-CERT-000000000000` | oui |

Le numéro de série est celui que porte le QR code imprimé sur l'article. Le
serveur le canonicalise avant la recherche : il ramène les lettres `I` et `L`
au chiffre `1`, et la lettre `O` au chiffre `0`. Le serveur reconnaît donc
quand même un numéro ressaisi à la main avec un `I`, un `L` ou un `O` à la
place d'un `1` ou d'un `0`.

Une forme qui ne trouve rien n'arrête pas la recherche. Une valeur de 66
caractères commençant par `0x` qui ne correspond à aucune empreinte est ensuite
essayée comme identifiant de jeton, puis comme numéro de série, puis comme
numéro de certificat, avant le 404.

Seuls les articles encore au catalogue répondent. Le serveur traite comme
introuvables un article détruit, un article remplacé par une frappe ultérieure
et un article archivé.

Si plusieurs enregistrements correspondent à une empreinte, à un identifiant de
jeton ou à un numéro de série, le serveur rend le plus récemment créé. Un
numéro de certificat est unique, il désigne un seul article.

## Corps de la requête

Aucun. C'est une requête `GET`, tout passe par le chemin.

## Requête d'exemple

:::onglets
```bash title="curl"
curl -i https://api.sealtrust.io/v1/resolve/0x0000000000000000000000000000000000000000000000000000000000000000
```
```typescript
const identifiant =
  "0x0000000000000000000000000000000000000000000000000000000000000000";

const response = await fetch(
  `https://api.sealtrust.io/v1/resolve/${encodeURIComponent(identifiant)}`,
);

console.log(response.status);
console.log(response.headers.get("X-RateLimit-Remaining") ?? "inconnu");
console.log(await response.json());
```
```python
import requests
from urllib.parse import quote

identifiant = "0x0000000000000000000000000000000000000000000000000000000000000000"

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

print(response.status_code)
print(response.headers.get("X-RateLimit-Remaining", "inconnu"))
print(response.json())
```
:::

> [!INFO] Le SDK TypeScript ne couvre pas ce point d'entrée
> Le [SDK TypeScript](/sdk-typescript/), paquet `@sealtrust-io/sdk`, expose
> l'historique d'un article, la vérification par lot, le contrôle d'intégrité
> des métadonnées, la gestion complète des abonnements webhook, l'appel de
> frappe en lot du partenaire et le suivi de sa tâche. Il n'expose aucune
> méthode de résolution universelle. Vous appelez donc ce point d'entrée en
> HTTP direct, comme ci-dessus.

## Réponse d'exemple

Code HTTP 200. Un article complet, avec certificat, passeport publié, un média,
un événement et les deux preuves d'ancrage.

```json
{
  "token_id": "1024",
  "uid_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "product_name": "Sac de voyage Exemple SAS",
  "brand_name": "Exemple SAS",
  "category_name": "Maroquinerie",
  "sku": null,
  "description": null,
  "metadata_uri": "ipfs://bafyexemple00000000000000000000000000000000000000000000000",
  "created_at": "2026-08-14T09:12:33.418000Z",
  "tx_hash": "0x4444444444444444444444444444444444444444444444444444444444444444",
  "contract_address": "0x0000000000000000000000000000000000000000",
  "certificate": {
    "certificate_number": "ST-CERT-000000000000",
    "status": "active",
    "issued_at": "2026-08-14T09:14:02.117043Z",
    "expires_at": null,
    "issuer_name": "Exemple SAS"
  },
  "passport": {
    "schema_version": "1.0",
    "passport_version": 3,
    "data": {
      "product_identity": {
        "gtin": "03701234567890",
        "model": "Sac de voyage",
        "brand": "Exemple SAS",
        "made_in": "FR",
        "production_facility": "Atelier Exemple SAS"
      },
      "materials": {
        "primary": {
          "name": "Full grain leather",
          "percentage": 70,
          "origin": "IT"
        },
        "certified_organic": false
      },
      "environmental_impact": {
        "carbon_footprint_kg_co2e": 18.7,
        "water_usage_liters": 2340,
        "energy_consumption_kwh": 45.2,
        "transport_distance_km": 850,
        "transport_mode": "road"
      },
      "circularity": {
        "recyclability_percentage": 62,
        "recycled_content_percentage": 0,
        "repairability_index": 7.8,
        "expected_lifetime_years": 15,
        "disassembly_instructions_url": "",
        "take_back_program": true
      },
      "compliance": {
        "eu_espr": true,
        "reach": true
      }
    },
    "published_at": "2026-08-18T07:03:11.902000Z",
    "data_hash": "0000000000000000000000000000000000000000000000000000000000000000"
  },
  "media": [
    {
      "id": 4821,
      "file_name": "sac-face.jpg",
      "media_type": "image",
      "url": "https://cdn.example.com/exemple-sas/sac-face.jpg",
      "alt_text": "Sac de voyage vu de face"
    }
  ],
  "events": [
    {
      "id": 9137,
      "event_type": "warranty_activation",
      "description": "Garantie activée à l'achat en boutique.",
      "occurred_at": "2026-08-19T14:32:07.481920Z",
      "actor_name": "Boutique Exemple SAS Lyon"
    }
  ],
  "merkle_anchor": {
    "anchor_tx_hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
    "root": "0x2222222222222222222222222222222222222222222222222222222222222222",
    "leaf_index": 17,
    "leaf": "0x3333333333333333333333333333333333333333333333333333333333333333",
    "proof": [
      "0x5555555555555555555555555555555555555555555555555555555555555555",
      "0x6666666666666666666666666666666666666666666666666666666666666666"
    ]
  },
  "passport_anchor": {
    "tx_hash": "0x7777777777777777777777777777777777777777777777777777777777777777",
    "basescan_url": "https://basescan.org/tx/0x7777777777777777777777777777777777777777777777777777777777777777",
    "merkle_root": "0x2222222222222222222222222222222222222222222222222222222222222222",
    "anchored_at": "2026-08-18T07:05:44.220118Z",
    "passport_version": 3,
    "data_hash_matches": true
  }
}
```

Code HTTP 200 également pour un article minimal. Un article sans certificat en
cours, sans passeport publié, sans média, sans historique et dont le lot n'a
pas été ancré rend la même structure avec des valeurs vides.

```json
{
  "token_id": null,
  "uid_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "product_name": "Sac de voyage Exemple SAS",
  "brand_name": "Exemple SAS",
  "category_name": null,
  "sku": null,
  "description": null,
  "metadata_uri": "ipfs://bafyexemple00000000000000000000000000000000000000000000000",
  "created_at": "2026-08-14T09:12:33.418000Z",
  "tx_hash": null,
  "contract_address": "0x0000000000000000000000000000000000000000",
  "certificate": null,
  "passport": null,
  "media": [],
  "events": [],
  "merkle_anchor": null,
  "passport_anchor": null
}
```

### Les champs de premier niveau

| Champ | Type | Description |
| --- | --- | --- |
| `token_id` | `string` ou `null` | L'identifiant du jeton, sous forme de chaîne. `null` tant que la frappe n'a pas été confirmée sur la chaîne. |
| `uid_hash` | `string` ou `null` | L'empreinte de l'article, telle qu'elle est enregistrée. |
| `product_name` | `string` ou `null` | Le nom de l'article. |
| `brand_name` | `string` ou `null` | Le nom de la marque propriétaire. `null` si aucune marque n'est rattachée. |
| `category_name` | `string` ou `null` | Le nom de la catégorie. `null` si aucune catégorie n'est rattachée. |
| `sku` | `null` | Toujours `null`. Le champ figure dans la réponse et n'est jamais renseigné par ce point d'entrée. |
| `description` | `null` | Toujours `null`. Même remarque que pour `sku`. |
| `metadata_uri` | `string` ou `null` | L'adresse des métadonnées de l'article. |
| `created_at` | `string` ou `null` | Date de création de l'enregistrement, au format ISO 8601 en temps universel. |
| `tx_hash` | `string` ou `null` | La transaction de frappe. Elle ne change pas quand l'article est transféré, donc c'est le lien de preuve d'origine. |
| `contract_address` | `string` ou `null` | L'adresse du contrat qui porte ce jeton. |
| `certificate` | objet ou `null` | Le certificat en cours de validité. `null` si l'article n'en a aucun. |
| `passport` | objet ou `null` | Le passeport publié, filtré au niveau consommateur. `null` si aucun passeport publié n'existe. |
| `media` | tableau | Les médias de l'article. Tableau vide si aucun. |
| `events` | tableau | L'historique public de l'article. Tableau vide si aucun. |
| `merkle_anchor` | objet ou `null` | La preuve d'appartenance de l'article à un lot ancré sur Base. |
| `passport_anchor` | objet ou `null` | L'ancrage du contenu du passeport. |

### `certificate`

Le serveur rend le certificat le plus récemment émis parmi ceux qui sont
encore actifs à l'instant de l'appel. Il écarte un certificat révoqué, et il
écarte un certificat dont la date d'expiration est passée.

| Champ | Type | Description |
| --- | --- | --- |
| `certificate_number` | `string` | Le numéro du certificat. C'est aussi l'une des quatre formes d'identifiant acceptées par ce point d'entrée. |
| `status` | `string` | Vaut toujours `active` sur ce point d'entrée. Un certificat révoqué ou périmé n'est pas rendu ici, le champ `certificate` vaut alors `null`. Pour lire l'état `revoked` ou `expired`, appelez `GET /v1/certificate/{identifier}`. |
| `issued_at` | `string` | Date d'émission, au format ISO 8601. |
| `expires_at` | `string` ou `null` | Date de fin de validité. Aucun certificat émis par la plateforme n'en porte aujourd'hui, la valeur est toujours `null`. Ne construisez pas votre intégration sur une date de fin. |
| `issuer_name` | `string` ou `null` | Le nom de la marque qui a émis le certificat. Le serveur calcule ce champ à la lecture. `null` quand le certificat n'est rattaché à aucune marque. |

### `passport`

Le passeport rendu est celui qui porte le numéro de version le plus élevé
parmi les versions publiées. Le serveur cherche d'abord un passeport rattaché
au modèle de l'article, puis un passeport rattaché à l'article lui-même. Le
serveur ne rend ici ni les brouillons, ni les passeports réservés à la
marque.

| Champ | Type | Description |
| --- | --- | --- |
| `schema_version` | `string` | La version du schéma de données du passeport. |
| `passport_version` | `integer` | Le numéro de version du passeport, incrémenté à chaque publication. |
| `data` | objet | Le contenu du passeport, filtré au niveau consommateur. Sa structure dépend de la catégorie de produit et de ce que la marque a rempli. |
| `published_at` | `string` ou `null` | Date de publication de cette version. |
| `data_hash` | `string` ou `null` | L'empreinte du contenu, 64 caractères hexadécimaux, sans préfixe `0x`. Cette empreinte entre dans la feuille de l'arbre dont la racine est inscrite sur la chaîne. Le champ `passport_anchor.merkle_root` rend cette racine. |

Le filtrage retient les sections destinées au public et au client final,
quand le passeport les contient : identité du produit, conformité ESPR,
conformité REACH, marquage CE, étiquettes, spécification de batterie,
circularité, impact environnemental, durabilité, efficacité énergétique,
empreinte carbone, matières principales et mention de certification biologique.

Le serveur retire tout le reste avant l'envoi, y compris à l'intérieur d'une
section partiellement retenue. Dans l'exemple ci-dessus, la section `materials`
du passeport complet décrit aussi la doublure et la quincaillerie : ces deux
entrées ne sortent pas par ce point d'entrée.

Une marque peut définir ses propres règles d'accès pour ses groupes de
produits. Ces règles remplacent alors entièrement le découpage par défaut
décrit ci-dessus, y compris pour le niveau public : une règle publique posée
sur la nomenclature de fabrication la fait sortir par ce point d'entrée.

### `media`

Jusqu'à 20 entrées, dans l'ordre d'affichage défini par la marque.

Le serveur prend d'abord les médias rattachés à l'article. S'il n'y en a aucun,
il prend ceux du modèle. En dernier recours, il rend l'image de couverture du
modèle, seule, avec l'identifiant `0`. Cette valeur `0` signale une entrée
fabriquée pour l'occasion, sans enregistrement propre.

| Champ | Type | Description |
| --- | --- | --- |
| `id` | `integer` | L'identifiant du média. `0` pour l'image de couverture de dernier recours. |
| `file_name` | `string` | Le nom du fichier. |
| `media_type` | `string` | `image`, `video`, `document` ou `3d_model`. |
| `url` | `string` | L'adresse publique du fichier. Le serveur retire de la liste un média dont il ne peut pas construire l'adresse. |
| `alt_text` | `string` ou `null` | Le texte alternatif saisi par la marque. |

### `events`

Jusqu'à 20 entrées, de la plus récente à la plus ancienne. La liste est tirée
des 60 derniers événements enregistrés, puis nettoyée.

Le serveur retire deux familles d'événements. Les étapes de rachat qui n'ont
rien changé à l'objet : proposition, refus, expiration, accord non réglé. Et
les transferts de propriété, qui ne figurent pas dans cette liste.

| Champ | Type | Description |
| --- | --- | --- |
| `id` | `integer` | L'identifiant de l'événement. |
| `event_type` | `string` | Le type d'événement. Valeurs possibles : `repair`, `warranty_activation`, `warranty_extension`, `resale`, `return`, `inspection`, `recall`, `end_of_life`, `custom`, `quality_control`, `reconditioning`, `distribution`, `after_sale_service`, `maintenance`, `certification`, `recycling`, `donation`, `destruction`. |
| `description` | `string` ou `null` | Le texte libre saisi par l'auteur de l'événement. |
| `occurred_at` | `string` | Date de l'événement, au format ISO 8601. |
| `actor_name` | `string` ou `null` | Le nom de l'auteur de l'événement. Vaut `null` dès que la valeur enregistrée contient un `@`. Ce point d'entrée est entièrement public et accepte le numéro de série imprimé sur l'étiquette : tenir l'objet ne doit pas donner l'adresse e-mail de son propriétaire, et une adresse tronquée se devinerait. Un nom choisi par l'acteur, lui, reste affiché. |

### `merkle_anchor`

Présent seulement si trois conditions sont réunies : l'article appartient à un
lot dont la racine a été ancrée sur Base, l'article porte un identifiant de
jeton, et la racine recalculée aujourd'hui est identique à la racine ancrée. Si
le lot a changé depuis l'ancrage, la preuve serait invérifiable sur la chaîne
et le champ vaut `null`. Le serveur ne rend jamais une preuve trompeuse.

| Champ | Type | Description |
| --- | --- | --- |
| `anchor_tx_hash` | `string` | La transaction qui a inscrit la racine sur Base. |
| `root` | `string` | La racine ancrée. |
| `leaf_index` | `integer` | La position de la feuille de cet article, comptée à partir de 0. |
| `leaf` | `string` | L'empreinte de la feuille de cet article. |
| `proof` | tableau de `string` | Les empreintes sœurs, de bas en haut, qui permettent de recalculer la racine à partir de la feuille. |

Vous pouvez vérifier cette preuve vous-même, sans nous faire confiance. La
recomposition part de `leaf`, applique les entrées de `proof` dans l'ordre, et
doit aboutir à `root`.

L'empreinte utilisée est keccak256. La convention de couple est celle
d'OpenZeppelin : à chaque étage, vous concaténez les 32 octets de la valeur
courante et les 32 octets de l'entrée de `proof` dans l'ordre croissant, puis
vous appliquez keccak256 au résultat.

```python title="Recomposer la racine"
from eth_utils import keccak  # pip install eth-utils

root = "0x2222222222222222222222222222222222222222222222222222222222222222"
leaf = "0x3333333333333333333333333333333333333333333333333333333333333333"
proof = [
    "0x5555555555555555555555555555555555555555555555555555555555555555",
    "0x6666666666666666666666666666666666666666666666666666666666666666",
]

node = bytes.fromhex(leaf[2:])
for entree in proof:
    voisin = bytes.fromhex(entree[2:])
    node = keccak(node + voisin) if node < voisin else keccak(voisin + node)

print("0x" + node.hex() == root)
```

Les valeurs ci-dessus sont inventées, donc ce programme affiche `False`.
Remplacez-les par celles d'une réponse réelle et il affiche `True`.

### `passport_anchor`

Présent seulement si cette version exacte du passeport a été ancrée et si la
transaction d'ancrage existe. Ce champ date le contenu du passeport. Le champ
`merkle_anchor` ci-dessus date l'article. Les deux restent séparés parce qu'ils
ne prouvent pas la même chose.

| Champ | Type | Description |
| --- | --- | --- |
| `tx_hash` | `string` | La transaction qui a inscrit la racine sur Base. |
| `basescan_url` | `string` ou `null` | Le lien direct vers cette transaction sur l'explorateur de la chaîne. |
| `merkle_root` | `string` ou `null` | La racine ancrée. |
| `anchored_at` | `string` ou `null` | Date de l'ancrage, au format ISO 8601. |
| `passport_version` | `integer` ou `null` | La version du passeport couverte par cet ancrage. |
| `data_hash_matches` | `boolean` ou `null` | `true` quand le contenu stocké aujourd'hui correspond à ce qui a été ancré. `false` signale que le passeport a changé depuis. |

> [!ATTENTION] Traitez `data_hash_matches: false` comme un signal
> Cette valeur signifie que le passeport publié aujourd'hui n'est plus celui
> dont l'empreinte a été inscrite sur la chaîne. Une interface qui affiche
> l'ancrage doit afficher cet écart. Le masquer reviendrait à présenter une
> preuve qui ne couvre pas le contenu montré.

Aucune preuve d'inclusion n'accompagne l'ancrage du passeport. Pour vérifier
l'appartenance vous-même, appelez `GET /v1/passport/{identifier}/proof`, qui
rend la feuille, sa position et les empreintes sœurs.

## Erreurs

| Code | Condition | Que faire |
| --- | --- | --- |
| 404 | Aucun article au catalogue ne correspond à cet identifiant, dans aucune des quatre formes. Message `Product not found`. Un article détruit, remplacé ou archivé donne la même réponse. | Vérifiez la valeur envoyée. Si l'article a été détruit, remplacé ou retiré du catalogue, ce code est définitif. |
| 404 | Aucun identifiant n'a été fourni, l'appel s'arrête à `/v1/resolve`. Message `Not Found`. | Ajoutez l'identifiant dans le chemin. |
| 405 | Une méthode autre que `GET` a été envoyée sur ce chemin. Message `Method Not Allowed`. La réponse porte l'en-tête `Allow: GET`. | Ce point d'entrée ne répond qu'en `GET`. |
| 429 | Plus de 60 appels en 60 secondes depuis la même adresse IP. Message `Rate limit exceeded: 60 requests per 60s`. | Attendez le nombre de secondes indiqué par `Retry-After`. Répartissez vos appels dans le temps. |
| 500 | Erreur inattendue du serveur. Corps figé `{"detail": "Internal Server Error"}`. | Réessayez. L'en-tête `X-Request-Id` identifie l'appel, transmettez-le nous s'il se répète. |

Une valeur qui ne ressemble à aucune des quatre formes attendues reçoit un
`404`.

> [!INFO] Un problème d'ancrage ne fait pas échouer l'appel
> Un échec du calcul de l'une des deux preuves d'ancrage ne fait pas tomber
> l'appel. Le champ concerné vaut alors `null` et le serveur rend le reste de
> la réponse normalement, en 200. Traitez donc l'absence d'ancrage comme un
> cas ordinaire dans votre interface.

## Voir aussi

- [`GET /products/{uid}/public`](/reference/get-products-uid-public/),
  lire les informations publiques d'un produit.
- [`GET /timeline/{identifier}`](/reference/get-timeline/),
  lire l'historique public d'un produit.
- [`GET /certificate/{identifier}`](/reference/get-certificate/),
  lire le certificat d'authenticité d'un article.
- [`GET /passport/{identifier}/proof`](/reference/get-passport-proof/),
  rassembler les preuves publiques du passeport d'un article.
