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.

Sur cette page

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 cheminCe qu'il faitDroit
GET /partner/passports/{identifier}le passeport d'un exemplaire, sa preuve et sa signaturepassport:read
GET /partner/passports/01/{gtin}le passeport d'un modèle et sa preuvepassport:read
GET /partner/passports/01/{gtin}/10/{lot}le passeport d'un lot et sa preuvepassport:read
POST /partner/scansenregistre le scan d'un de vos visiteursscans:write
POST /partner/nfc/verifyvérifie une lecture de puce pour un de vos visiteursnfc:verify
GET /partner/nfc/receipts/{receipt}lit le résultat d'une lecture que notre page a vérifiéenfc: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.

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êteCe qu'il contient
X-SealTrust-Visitor-IPl'adresse de votre visiteur, telle que votre serveur l'a vue
X-SealTrust-Visitor-UAl'agent de son navigateur, facultatif
X-SealTrust-Visitor-Timestampl'heure de la signature, en secondes depuis 1970
X-SealTrust-Visitor-Signaturev1= 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 :

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ètreValeur
levelunit, model ou lot
serialle numéro de série, pour un exemplaire
gtin et lotpour un modèle ou un lot
langfr 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.

Texte
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 :

Texte
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. Ces six points d'entrée ne consomment pas le quota quotidien de la clef.

#Erreurs

CodeConditionQue faire
400Les en-têtes du visiteur manquent.Envoyez les quatre en-têtes décrits plus haut.
401Clef 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é.
403Le droit manque, ou l'offre ne comprend pas l'accès API.Créez une clef qui porte ce droit.
403SDM_MAC_MISMATCH : la signature de la puce ne se vérifie pas.La lecture n'est pas celle d'une puce authentique.
404Identifiant 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.
409Cette lecture de puce a déjà été vérifiée ailleurs.Si notre page l'a vérifiée, lisez son reçu.
422Corps 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.
429Plafond de débit atteint.Attendez le délai indiqué par Retry-After.
503NFC_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

Votre réponse ouvre un courriel pré-rempli dans votre messagerie, à destination de contact@sealtrust.io. Vous le relisez avant de l'envoyer.

Proposer une correctionSignaler un problème