# Afficher le passeport dans votre page

Afficher le passeport d'une marque dans votre propre page sans rien perdre : le passeport, sa preuve et sa signature en un appel, les scans et les lectures NFC de vos visiteurs comptés pour eux, le QR imprimé qui mène à votre page, et la revendication et le transfert dans une fenêtre hébergée qui revient chez vous. Droits passport:read, scans:write et nfc:verify.

Source : https://docs.sealtrust.io/api-affichage-partenaire/

---

Cette page s'adresse au partenaire qui affiche les passeports d'une marque dans
sa propre page, avec son propre habillage : un revendeur pour ses marques
clientes, une agence pour la marque qu'elle sert. En la quittant, vous saurez
lire un passeport complet en un appel, faire compter chaque scan pour le
visiteur qui l'a fait, vérifier une puce NFC une seule fois, recevoir les scans
du QR imprimé sur votre page, et faire revendiquer un produit sans jamais
demander le mot de passe SealTrust de votre visiteur.

La page hébergée par SealTrust reste l'option par défaut. Rien de ce qui suit
ne change pour une marque qui ne l'a pas demandé.

## Ce qu'il vous faut

- Une clef d'API qui porte les droits dont vous avez besoin : `passport:read`
  pour lire, `scans:write` pour enregistrer les scans, `nfc:verify` pour les
  puces NFC. Vous la créez dans la console, Paramètres puis Développeurs.
- Une offre qui comprend l'accès API.
- Pour un revendeur, une clef revendeur : elle agit pour votre marque et pour
  chacune des marques clientes de votre contrat.
- Votre serveur. Tous ces appels partent de votre serveur, jamais du
  navigateur de votre visiteur : votre clef ne doit jamais atteindre une page.

## Les six points d'entrée

Toutes les adresses existent aussi avec le préfixe `/v1`, qui est la forme
recommandée.

| Méthode et chemin | Ce qu'il fait | Droit |
| --- | --- | --- |
| `GET /partner/passports/{identifier}` | le passeport d'un exemplaire, sa preuve et sa signature | `passport:read` |
| `GET /partner/passports/01/{gtin}` | le passeport d'un modèle et sa preuve | `passport:read` |
| `GET /partner/passports/01/{gtin}/10/{lot}` | le passeport d'un lot et sa preuve | `passport:read` |
| `POST /partner/scans` | enregistre le scan d'un de vos visiteurs | `scans:write` |
| `POST /partner/nfc/verify` | vérifie une lecture de puce pour un de vos visiteurs | `nfc:verify` |
| `GET /partner/nfc/receipts/{receipt}` | lit le résultat d'une lecture que notre page a vérifiée | `nfc:verify` |

Chaque point d'entrée ne répond que pour les marques que votre clef peut
servir. Un produit d'une autre marque reçoit le même 404 qu'un identifiant qui
n'existe pas.

## Le passeport, sa preuve et sa signature en un appel

`{identifier}` est le numéro de série imprimé sur le produit, son empreinte de
puce ou son numéro de jeton.

```bash title="curl"
curl https://api.sealtrust.io/v1/partner/passports/K4QNAFCHDETK \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000"
```

```json
{
  "level": "unit",
  "serial": "K4QNAFCHDETK",
  "passport": { "data": {}, "data_hash": "…", "gs1_digital_link": "…" },
  "proof": { "data_hash": "…", "anchor": {}, "vc": {} },
  "credential": { "format": "dc+sd-jwt", "issuer": "did:web:…", "sd_jwt_vc": "…" }
}
```

- `passport` est exactement la réponse publique de
  `GET /passport/{identifier}` au niveau d'accès `public`.
- `proof` est exactement celle de `GET /passport/{identifier}/proof`.
- `credential` est exactement celle de `GET /passport/{identifier}/vc`, ou
  `null` tant que la marque n'a pas émis d'attestation signée pour ce
  passeport.

Votre page montre donc ce que la page hébergée montre à un visiteur anonyme,
ni plus ni moins. Pour un modèle ou un lot, `credential` vaut toujours `null`,
et `level` vaut `model` ou `lot`.

## Compter chaque scan pour le visiteur qui l'a fait

Vos appels partent de votre serveur, donc de votre adresse. Sans autre
information, tous vos visiteurs auraient votre adresse : deux personnes qui
scannent le même produit dans la même demi-heure compteraient pour un seul
scan, et chaque scan serait situé chez votre hébergeur.

`POST /partner/scans` et `POST /partner/nfc/verify` demandent donc quatre
en-têtes qui nomment le visiteur.

| En-tête | Ce qu'il contient |
| --- | --- |
| `X-SealTrust-Visitor-IP` | l'adresse de votre visiteur, telle que votre serveur l'a vue |
| `X-SealTrust-Visitor-UA` | l'agent de son navigateur, facultatif |
| `X-SealTrust-Visitor-Timestamp` | l'heure de la signature, en secondes depuis 1970 |
| `X-SealTrust-Visitor-Signature` | `v1=` suivi de la signature, en hexadécimal |

La signature est un HMAC-SHA256. Sa clef est l'empreinte SHA-256 de votre
clef d'API, écrite en hexadécimal minuscule. Le message signé est fait de six
lignes séparées par un saut de ligne : `v1`, l'heure, la méthode en
majuscules, le chemin appelé sans sa partie après `?`, l'adresse du visiteur,
et son agent (une ligne vide s'il n'y en a pas).

```python
import hashlib, hmac, time, requests

API_KEY = "st_live_0000000000000000000000000000000000000000000000"
path = "/v1/partner/scans"
ip, ua = "203.0.113.77", "Mozilla/5.0"
ts = str(int(time.time()))
key = hashlib.sha256(API_KEY.encode()).hexdigest().encode()
message = "\n".join(["v1", ts, "POST", path, ip, ua]).encode()
signature = hmac.new(key, message, hashlib.sha256).hexdigest()

requests.post(
    "https://api.sealtrust.io" + path,
    json={"serial": "K4QNAFCHDETK"},
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "X-SealTrust-Visitor-IP": ip,
        "X-SealTrust-Visitor-UA": ua,
        "X-SealTrust-Visitor-Timestamp": ts,
        "X-SealTrust-Visitor-Signature": f"v1={signature}",
    },
    timeout=10,
)
```

```javascript
import { createHash, createHmac } from "node:crypto";

const apiKey = "st_live_0000000000000000000000000000000000000000000000";
const path = "/v1/partner/scans";
const ip = "203.0.113.77";
const ua = "Mozilla/5.0";
const ts = String(Math.floor(Date.now() / 1000));
const key = createHash("sha256").update(apiKey).digest("hex");
const signature = createHmac("sha256", key)
  .update(["v1", ts, "POST", path, ip, ua].join("\n"))
  .digest("hex");

await fetch("https://api.sealtrust.io" + path, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${apiKey}`,
    "X-SealTrust-Visitor-IP": ip,
    "X-SealTrust-Visitor-UA": ua,
    "X-SealTrust-Visitor-Timestamp": ts,
    "X-SealTrust-Visitor-Signature": `v1=${signature}`,
  },
  body: JSON.stringify({ serial: "K4QNAFCHDETK" }),
});
```

Une signature vaut pour une méthode, un chemin et cinq minutes. Signez chaque
appel au moment de l'envoyer.

Le corps de `POST /partner/scans` nomme le produit par `serial` ou par
`uid_hash`, jamais les deux. Il accepte aussi `latitude` et `longitude` quand
l'appareil du visiteur les a données. Nous ne gardons de l'adresse qu'une ville
et un pays, et nous enregistrons une adresse anonymisée.

Un même visiteur qui revient sur le même produit dans la demi-heure compte pour
un seul scan. La réponse vaut alors `{"ok": true, "deduped": true}`.

## Une seule vérification NFC par lecture

Une puce NFC produit, à chaque lecture, une adresse à usage unique. La
vérifier deux fois fait répondre la seconde vérification comme une copie de
l'adresse, ce qui s'affiche comme un possible clone d'un produit authentique.
Deux cas se présentent.

**La lecture ouvre notre page.** C'est le cas des puces déjà en circulation :
leur adresse mène à SealTrust. Quand la marque affiche ses passeports chez
vous, notre page vérifie la lecture une fois, puis envoie le visiteur sur votre
page avec `level=nfc`, `serial` et `receipt`. Votre serveur lit le résultat
avec ce reçu :

```bash title="curl"
curl https://api.sealtrust.io/v1/partner/nfc/receipts/Zr3Kx0vQ8a1bW9c2dE7fG4hJ6kL5mN0p \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000"
```

Ne renvoyez jamais cette lecture à `POST /partner/nfc/verify` : elle a déjà été
vérifiée. Un reçu se lit pendant dix minutes, autant de fois que nécessaire.

**Votre propre lecteur a lu la puce.** Envoyez `e`, `c` et, pour une puce
scellée, `t`, tels que la puce les a écrits, avec les quatre en-têtes du
visiteur, à `POST /partner/nfc/verify`. Si votre appel échoue en route,
renvoyez la même lecture avec la même clef : pendant dix minutes, la réponse
redonne le premier résultat avec `repeat: true`, au lieu de le prendre pour
une copie. Si votre premier appel est encore en cours quand la relance
arrive, la relance attend son résultat quelques secondes, ou répond `503`
`NFC_READ_IN_PROGRESS` avec un en-tête `Retry-After` : renvoyez-la après ce
délai, elle redonnera le premier résultat.

Les deux réponses portent les mêmes champs : `verified`, `serial`, `uid_hash`,
`ctr`, `brand_code`, `brand_id`, `product_name`, `token_id`, `declared_stolen`,
`tamper_status`, `seal_intact`, `tamper_policy`, `scan_area` et `verified_at`.
`brand_code` est le code public de la marque. `brand_id`, son numéro interne,
est déprécié : lisez `brand_code`.
Pour le passeport lui-même, appelez ensuite
`GET /partner/passports/{serial}`.

## Le QR imprimé mène à votre page

Dans la console, Paramètres puis Développeurs, le bloc « Passeport affiché dans
la page d'un partenaire » déclare vos adresses web et la page qui affiche un
passeport. Une case envoie ensuite les scans du QR imprimé vers cette page.
Un revendeur le règle une fois sur sa propre marque, et le réglage vaut pour
toutes les marques clientes de son contrat, sauf celles qui ont le leur.

Les liens imprimés `/p/{serial}`, `/01/{gtin}/21/{serial}`, `/01/{gtin}` et
`/01/{gtin}/10/{lot}` répondent alors `302` vers votre page, avec ces
paramètres :

| Paramètre | Valeur |
| --- | --- |
| `level` | `unit`, `model` ou `lot` |
| `serial` | le numéro de série, pour un exemplaire |
| `gtin` et `lot` | pour un modèle ou un lot |
| `lang` | `fr` ou `en`, la langue du lecteur |

Deux demandes gardent notre passeport : une machine qui demande le passeport
lui-même (`?linkType=dpp`), et un exemplaire retiré. Le scan d'un exemplaire
est enregistré au moment de la redirection, pour le visiteur. Si votre page
l'enregistre aussi avec `POST /partner/scans` pour le même visiteur, il ne
compte qu'une fois. Les robots qui suivent un lien pour en afficher l'aperçu
(WhatsApp, Slack, LinkedIn, moteurs de recherche) sont redirigés, mais ne
comptent pas comme un scan.

Une marque cliente dont l'offre ne comprend pas l'accès API voit quand même,
dans ce bloc, que ses scans partent vers la page de son revendeur. Elle peut
les garder sur la page hébergée : le bouton « Garder les scans du QR sur la
page hébergée » ne demande pas l'accès API.

## La revendication dans une fenêtre hébergée

Revendiquer un produit demande un compte SealTrust. Ne demandez jamais le mot
de passe de votre visiteur : ouvrez notre page de revendication, dans une
nouvelle fenêtre ou en y envoyant le visiteur.

```text
https://sealtrust.io/fr/claim?serial=K4QNAFCHDETK&return=https%3A%2F%2Fvotre-site.example%2Fpasseport
```

Un visiteur qui n'a pas encore de compte le crée depuis cette page. Le numéro
de série et `return` le suivent jusqu'au courriel de confirmation, puis jusqu'à
la connexion : il revient sur l'écran de revendication de ce produit, sans
rien ressaisir. Les liens « Se connecter » et « S'inscrire » du haut de la page
les portent aussi dès l'affichage, avant même la fin du chargement : un
visiteur sur un réseau mobile lent qui clique tout de suite ne perd rien.

`return` est la page où revenir. Elle doit se trouver sur l'une des adresses
web déclarées dans la console pour cette marque, ou sur le domaine vérifié qui
sert ses pages publiques, sinon aucun bouton de retour n'apparaît. Quand vous venez de lire la puce, ajoutez la lecture après un `#`,
par exemple `#e=…&c=…`, pour que le visiteur n'ait pas à la relire : la
lecture ne quitte pas son navigateur.

Notre page ne revendique jamais seule. Elle ne prend la lecture passée après
le `#` que si `return` est accepté pour ce produit, puis affiche un bouton
« Revendiquer ce produit » : la revendication part au clic du visiteur, pas
avant. Sans `return` accepté, la lecture est ignorée.

Cette lecture sert pendant cinq minutes après sa vérification. Passé ce délai,
ou si le visiteur met plus longtemps à se connecter, il relit la puce sur notre
page. C'est la même règle que sur la page hébergée : une lecture de puce ne se
rejoue pas au-delà de quelques minutes.

Une fois la revendication faite, le bouton « Retour vers votre-site.example »
ramène le visiteur :

- si vous avez ouvert la fenêtre avec `window.open`, votre page reçoit un
  message `{ "type": "passport-claim", "outcome": "claimed", "serial": "K4QNAFCHDETK" }`
  adressé à l'origine exacte de la page `return`, et la fenêtre se ferme.
  Ouvrez donc la fenêtre depuis une page de cette même origine ;
- sinon, le visiteur revient sur `return`, avec `claim_outcome=claimed` ajouté
  à l'adresse.

`outcome` prend les valeurs `claimed`, `submitted`, `already_owned`,
`expired`, `not_activated`, `window_closed`, `code_not_issued`, `unavailable`
ou `failed`.

## Le transfert dans une fenêtre hébergée

Le propriétaire transfère son produit de la même façon, sur notre page de
transfert, avec les mêmes paramètres :

```text
https://sealtrust.io/fr/transfer?serial=K4QNAFCHDETK&return=https%3A%2F%2Fvotre-site.example%2Fpasseport
```

Le produit nommé par `serial` est présélectionné s'il appartient au visiteur
connecté. Un visiteur qui n'est pas connecté passe d'abord par la page de
connexion, puis revient sur cette même adresse, `serial` et `return` compris.
`return` suit la même règle que pour la revendication. Deux boutons
ramènent le visiteur : « Retour vers votre-site.example » une fois le transfert
fait, et « Retour vers votre-site.example sans transférer » avant.

- si vous avez ouvert la fenêtre avec `window.open`, votre page reçoit un
  message `{ "type": "passport-transfer", "outcome": "transferred", "serial": "K4QNAFCHDETK" }`
  adressé à l'origine exacte de la page `return`, et la fenêtre se ferme ;
- sinon, le visiteur revient sur `return`, avec `transfer_outcome=transferred`
  ajouté à l'adresse.

`outcome` vaut `transferred` (transfert fait), `pending_acceptance` (le
destinataire doit encore accepter) ou `cancelled` (le visiteur est revenu sans
transférer). Le retour et le rachat se font depuis l'espace du propriétaire sur
la page hébergée.

## Plafonds

Le plafond de débit se compte par clef et par marque, jamais par adresse :
tous les visiteurs que votre serveur représente ne se partagent pas un seul
compteur. Il est décrit dans la [vue d'ensemble](/api-vue-ensemble/). Ces six
points d'entrée ne consomment pas le quota quotidien de la clef.

## Erreurs

| Code | Condition | Que faire |
| --- | --- | --- |
| 400 | Les en-têtes du visiteur manquent. | Envoyez les quatre en-têtes décrits plus haut. |
| 401 | Clef absente ou inconnue, signature fausse, ou heure à plus de cinq minutes de la nôtre. | Signez chaque appel au moment de l'envoi, avec le chemin exact appelé. |
| 403 | Le droit manque, ou l'offre ne comprend pas l'accès API. | Créez une clef qui porte ce droit. |
| 403 | `SDM_MAC_MISMATCH` : la signature de la puce ne se vérifie pas. | La lecture n'est pas celle d'une puce authentique. |
| 404 | Identifiant inconnu, passeport non publié, reçu expiré, ou produit d'une marque que votre clef ne sert pas. Pour une puce, la marque est vérifiée avant tout contrôle de rejeu : une puce d'une autre marque rend toujours `404`, jamais `409`, et sa lecture n'est pas consommée. | Vérifiez l'identifiant et la clef. |
| 409 | Cette lecture de puce a déjà été vérifiée ailleurs. | Si notre page l'a vérifiée, lisez son reçu. |
| 422 | Corps invalide, ou adresse de visiteur qui n'est pas publique : adresse locale, ou adresse d'un réseau privé (10.x, 172.16 à 172.31, 192.168.x, 100.64 à 100.127, IPv6 commençant par fc ou fd). | Envoyez l'adresse publique du visiteur. Derrière un répartiteur de charge, lisez-la dans l'en-tête que celui-ci ajoute, pas dans l'adresse de la connexion. |
| 429 | Plafond de débit atteint. | Attendez le délai indiqué par `Retry-After`. |
| 503 | `NFC_READ_IN_PROGRESS` : votre appel précédent vérifie encore la même lecture. | Renvoyez la même lecture après le délai indiqué par `Retry-After`. |

## Voir aussi

- [API partenaire, vue d'ensemble](/api-vue-ensemble/), clefs, droits et
  plafonds.
- [Remplir le catalogue par l'API](/api-catalogue/), la clef revendeur.
- [Clés d'API et webhooks](/console/reglages-developpeurs/), le réglage dans la
  console.
