Intégrer le SDK TypeScript
En quittant cette page, vous saurez installer le SDK TypeScript, créer le client, lire l'historique d'un produit, gérer vos abonnements aux notifications, reconnaître les deux familles d'erreurs, et faire tourner un programme complet.
Sur cette page
- Ce que le SDK couvre
- Installer
- Créer le client
- Lire l'historique d'un produit
- Deux méthodes qui refusent une clef d'API
- Gérer les abonnements aux notifications
- Supprimer un abonnement demande son adresse
- Envoyer un lot à l'API partenaire
- La clef d'idempotence de cette route
- Suivre l'avancement d'un lot
- Distinguer les deux familles d'erreurs
- Deux refus différents portent le code 429
- Exemple complet de bout en bout
- Les pièges connus, en un coup d'œil
Le SDK TypeScript est une bibliothèque publiée sur npm sous le nom
@sealtrust-io/sdk. Elle envoie les appels de l'API partenaire à votre place et
vous rend des objets typés. En quittant cette page, vous saurez l'installer,
créer le client, lire l'historique d'un produit, gérer vos abonnements aux
notifications, reconnaître les deux familles d'erreurs, et faire tourner un
programme complet qui déclare un abonnement puis lit une chronologie.
Vous saurez aussi créer des produits en lot par l'API, et ce que le serveur décide à votre place sur ce chemin.
Il vous faut d'abord une clef d'API. Vous la créez depuis la console de votre marque, dans Paramètres puis Développeurs, avec les droits dont vos appels ont besoin.
#Ce que le SDK couvre
Le SDK expose trois familles de méthodes, et rien d'autre. Chaque ligne du tableau ci-dessous a été vérifiée contre la route qu'elle appelle.
| Méthode | Route appelée | Droit requis sur la clef |
|---|---|---|
products.mint() | POST /partner/mint/batch | mint:batch |
products.getBatchStatus() | GET /partner/mint/batch/status/{job_id} | mint:batch |
webhooks.create() | POST /partner/webhooks | webhooks:write |
webhooks.list() | GET /partner/webhooks | webhooks:read |
webhooks.get() | GET /partner/webhooks/{webhook_id} | webhooks:read |
webhooks.update() | PUT /partner/webhooks/{webhook_id} | webhooks:write |
webhooks.delete() | DELETE /partner/webhooks/{webhook_id} | webhooks:write |
verify.timeline() | GET /timeline/{identifier} | aucun, route publique |
Deux méthodes supplémentaires existent dans le SDK et refusent une clef d'API :
verify.batch() et verify.metadataIntegrity(). Les routes qu'elles appellent
attendent une session de la console. Une clef d'API y revient en 401. Elles sont
décrites plus bas.
Une route de l'API partenaire n'a aucune méthode dans le SDK : la déclaration de
vente au client final,
POST /partner/sellout, qui demande le
droit sellout:write. Appelez-la avec fetch tant que le SDK ne la porte pas.
#Installer
La dernière version publiée est 0.3.0. Le paquet demande Node 18 ou plus
récent, parce qu'il utilise le fetch natif du langage.
npm install @sealtrust-io/sdkyarn add @sealtrust-io/sdkLe paquet vous donne deux formats, modules ES et CommonJS, avec ses définitions de types.
Les exemples de cette page utilisent setTimeout et process, qui appartiennent
à Node. Le paquet ne déclare pas les types de Node, donc votre compilateur les
ignore tant que vous ne les installez pas.
npm install --save-dev @types/node#Créer le client
Vous construisez le client une fois, avec votre clef, et vous le réutilisez pour tous vos appels.
import { SealTrustClient } from "@sealtrust-io/sdk";
const sealtrust = new SealTrustClient({
apiKey: process.env.SEALTRUST_API_KEY ?? "",
});Vous posez la variable SEALTRUST_API_KEY dans l'environnement de votre
serveur. Le nom est le vôtre, le SDK ne lit aucune variable de lui-même. Si la
variable est absente, la construction du client échoue sur-le-champ, avant tout
appel.
Le constructeur accepte quatre options.
| Option | Type | Obligatoire | Description |
|---|---|---|---|
apiKey | string | oui | Votre clef d'API. Elle part dans l'en-tête Authorization: Bearer <clef>. Une valeur vide fait échouer la construction du client sur-le-champ. |
baseUrl | string | non | L'adresse de l'API. Valeur par défaut https://api.sealtrust.io. |
timeout | number | non | Délai maximal d'un appel, en millisecondes. Valeur par défaut 30000. |
fetch | typeof fetch | non | L'implémentation de fetch à utiliser. Valeur par défaut celle du langage. |
L'option fetch sert à tester votre propre code sans toucher à une variable
globale. Vous lui passez une fonction qui rend la réponse de votre choix.
const clientDeTest = new SealTrustClient({
apiKey: "clef-factice-du-test",
fetch: async () =>
new Response(JSON.stringify({ token_id: "1", timeline: [] }), {
status: 200,
headers: { "content-type": "application/json" },
}),
});Ici la clef n'a aucune importance : la fonction fetch que vous fournissez rend
la réponse de votre choix et n'envoie la requête nulle part. Le constructeur
exige seulement qu'elle ne soit pas vide.
#Lire l'historique d'un produit
verify.timeline() rend les vérifications et les transferts de propriété d'un
produit, dans un seul objet.
Trois formes d'identifiant sont acceptées.
- Le numéro de série imprimé, celui que porte le QR code sur le produit. C'est le seul qu'un humain peut lire sur un objet.
- L'identifiant de jeton, en base dix.
- L'empreinte d'UID,
0xsuivi de 64 caractères hexadécimaux.
const histoire = await sealtrust.verify.timeline("EXEMP1E00001");
console.log(histoire.product_name);
console.log(histoire.brand_name);
for (const evenement of histoire.timeline) {
console.log(evenement.type, evenement.timestamp);
}Cette route est publique. Votre clef d'API n'y est ni exigée ni lue, et la réponse est la même avec ou sans elle. Elle porte son propre plafond, 30 appels par 60 secondes et par adresse d'appel, indépendant du quota de votre clef. Au-delà, elle répond 429. Un identifiant bien formé mais inconnu répond 404.
Le SDK refuse . et .. comme identifiants et lève une erreur avant tout
appel. Ces deux valeurs ne sont pas des identifiants valides, et laissées telles
quelles elles enverraient la requête vers une autre adresse que celle demandée.
#Deux méthodes qui refusent une clef d'API
verify.batch() et verify.metadataIntegrity() appellent des routes réelles.
Ces routes attendent une session de la console. Une clef d'API échoue à la
lecture de cette session et l'appel revient en 401. Le code renvoyé est celui
d'une authentification refusée, ce qui donne l'impression d'une clef cassée.
N'utilisez pas ces deux méthodes avec la clef de votre intégration serveur.
#Gérer les abonnements aux notifications
Les cinq méthodes de webhooks couvrent la totalité des routes d'abonnement de
l'API partenaire.
const abonnement = await sealtrust.webhooks.create({
url: "https://exemple-sas.example/webhooks/sealtrust",
events: ["product.minted", "product.transferred"],
secret: "secret-de-demonstration-a-remplacer",
});
console.log(abonnement.id, abonnement.events, abonnement.health);L'adresse doit commencer par https://. Le champ events est facultatif au
sens technique, et un abonnement créé sans lui ne reçoit jamais rien tout en
s'affichant en bonne santé. Renseignez-le toujours.
Le champ secret est le vôtre. L'API l'enregistre tel quel et signe chaque
livraison avec. Si vous ne l'envoyez pas, les livraisons partent sans signature.
Le champ health vaut healthy tant que les livraisons aboutissent, et
degraded quand la série de nouvelles tentatives abandonne votre adresse. Il
repasse à healthy à la première livraison réussie. C'est le champ à surveiller.
const page = await sealtrust.webhooks.list({ skip: 0, limit: 20 });
console.log(page.total);
for (const item of page.items) {
console.log(item.id, item.url, item.event_types, item.health);
}limit va de 1 à 100, et vaut 20 par défaut. Le corps de la réponse porte
total et items, et rien d'autre : les valeurs skip et limit que vous
avez envoyées ne reviennent pas.
#Supprimer un abonnement demande son adresse
webhooks.delete() prend deux arguments : l'identifiant, et l'adresse de ce
même abonnement. L'API compare l'adresse que vous envoyez à celle qu'elle a
enregistrée. Si les deux diffèrent, rien n'est supprimé et l'appel échoue en
400 avec le code CONFIRMATION_MISMATCH. Sans adresse du tout, le code est
CONFIRMATION_REQUIRED.
const cible = await sealtrust.webhooks.get(7);
await sealtrust.webhooks.delete(cible.id, cible.url);Supprimer un abonnement efface aussi le secret de signature enregistré. Vous devrez le renvoyer à la création suivante, et tant que vous ne l'avez pas fait, les livraisons partent sans signature. Il n'existe aucune annulation.
Le SDK ne va pas chercher l'adresse à votre place, volontairement. Le contrôle existe pour attraper un identifiant qui ne désigne pas ce que vous croyez, et une lecture faite sur ce même identifiant confirmerait le mauvais abonnement aussi bien que le bon.
Le message d'erreur ne contient jamais l'adresse attendue. Lisez-la avec
webhooks.get(id).
#Envoyer un lot à l'API partenaire
Ce point d'entrée crée des produits. Le serveur pose lui-même deux valeurs sur
chaque ligne : la méthode d'identification, qui vaut qr, et l'empreinte d'UID
de l'article. Votre ligne ne les porte pas et ne peut pas les porter, le schéma
de requête refuse tout champ qu'il ne connaît pas.
products.mint() accepte un objet seul ou un tableau d'objets. Le SDK
enveloppe l'objet seul dans un tableau avant l'envoi, le résultat est donc
identique.
Une ligne accepte cinq champs, et cinq seulement.
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
product_name | string | oui | Le nom du produit. |
brand_id | number | oui | Le numéro de votre marque. Il doit correspondre à la marque de la clef. |
category_id | number | oui | Le numéro de la catégorie. |
metadata_uri | string | oui | L'adresse des métadonnées du produit. |
external_ref | string | non | Votre propre référence. |
Un lot compte 500 articles au maximum. Une seule ligne portant le numéro d'une autre marque fait refuser le lot entier en 403.
const lot = await sealtrust.products.mint([
{
product_name: "Sac Exemple 001",
brand_id: 12,
category_id: 3,
metadata_uri: "ipfs://exemple-metadonnees-0001",
external_ref: "EX-0001",
},
]);
console.log(lot.job_id, lot.status, lot.items_count, lot.brand_id);Quatre conditions s'ajoutent au droit mint:batch de la clef, dans cet ordre.
- L'offre de votre marque doit comprendre l'accès à l'API. Sinon la réponse est
403 avec le code
FEATURE_NOT_AVAILABLE. - L'offre de votre marque doit autoriser la méthode
qr. Sinon le lot entier est refusé en 403 avec le codeAUTH_METHOD_NOT_ALLOWED. - Le forfait mensuel de produits de votre marque doit pouvoir absorber le lot
entier. Sinon la réponse est 403 avec le code
QUOTA_EXCEEDED, et le corps donnecurrent,additional,maxetperiod. - Le quota quotidien de la clef doit rester suffisant. Il compte un article par ligne. Un lot de 200 articles consomme 200 unités de quota. Sinon la réponse est 429. Le quota est consommé au moment où le lot est accepté, et il ne vous est pas rendu si le traitement échoue ensuite.
#La clef d'idempotence de cette route
POST /v1/partner/mint/batch est la seule route de l'API partenaire qui lit
l'en-tête Idempotency-Key. Le SDK pose cet en-tête sur chaque POST et chaque
PUT, et toutes les autres routes l'ignorent.
Quand vous n'en fournissez pas, le SDK en fabrique un au hasard, ce qui ne vous protège de rien : le SDK ne réessaie jamais de lui-même, un appel égale un envoi, et une clef tirée au hasard change à chaque envoi.
Pour qu'un nouvel essai après une coupure réseau retrouve le premier lot au lieu
d'en enfiler un second, fournissez votre propre clef, et gardez la même entre les
essais. products.mint() la prend en deuxième argument.
const lot = await sealtrust.products.mint(
[
{
product_name: "Sac Exemple 001",
brand_id: 12,
category_id: 3,
metadata_uri: "ipfs://exemple-metadonnees-0001",
external_ref: "EX-0001",
},
],
"exemple-lot-2026-08-20-001",
);L'API garde votre clef d'idempotence 24 heures. Rejouer la même clef avec le même lot rend la réponse du premier appel. Rejouer la même clef avec un lot différent renvoie 409.
#Suivre l'avancement d'un lot
products.getBatchStatus() prend l'identifiant rendu par products.mint().
Le champ status vaut queued en attente, started en cours, finished
terminé, failed échoué, et unknown quand le lot est introuvable. Le type
déclare quatre autres valeurs que la file d'exécution peut produire, ce qui
évite qu'un switch exhaustif refuse de compiler.
const etat = await sealtrust.products.getBatchStatus("9f2c4a7b1d3e5f60");
console.log(etat.status);Bouclez tant que le statut vaut queued ou started.
Quand la file a oublié un lot terminé mais que la réservation existe encore et
vous appartient, la réponse porte quatre champs en plus : batch_status,
items_count, success_count et error_count. Ils sont absents de toutes les
autres réponses, ce qui explique qu'ils soient facultatifs dans le type.
#Distinguer les deux familles d'erreurs
Le SDK lève deux classes d'erreur, et la différence commande des réactions opposées.
| Classe | Ce qui s'est passé | Ce que vous avez |
|---|---|---|
SealTrustError | L'API a répondu et a refusé. | status, body, headers. |
SealTrustNetworkError | L'API n'a pas pu être jointe, ou sa réponse n'a pas pu être lue. | cause. |
SealTrustNetworkError couvre trois cas : la panne de transport, le délai
dépassé, et un corps annoncé en JSON qui n'en est pas, y compris un corps vide
sur une réponse 200.
import {
SealTrustClient,
SealTrustError,
SealTrustNetworkError,
} from "@sealtrust-io/sdk";
const sealtrust = new SealTrustClient({
apiKey: process.env.SEALTRUST_API_KEY ?? "",
});
try {
await sealtrust.verify.timeline("EXEMP1E00001");
} catch (erreur) {
if (erreur instanceof SealTrustError) {
console.error(erreur.status, erreur.message);
console.error(erreur.headers.get("x-request-id"));
} else if (erreur instanceof SealTrustNetworkError) {
console.error("API injoignable :", erreur.message);
} else {
throw erreur;
}
}Le corps d'erreur de l'API porte un champ detail. C'est une chaîne dans la
plupart des refus, un objet quand la route refuse de façon structurée, et un
tableau de champs quand la validation échoue en 422. Quand c'est un objet, il
porte un champ code : FEATURE_NOT_AVAILABLE, AUTH_METHOD_NOT_ALLOWED,
QUOTA_EXCEEDED, CONFIRMATION_MISMATCH. Appuyez votre logique sur ce code. Le message, lui,
peut changer sans préavis.
import { SealTrustError } from "@sealtrust-io/sdk";
function codeDErreur(erreur: SealTrustError): string | null {
const detail = erreur.body?.detail;
if (detail && typeof detail === "object" && !Array.isArray(detail)) {
const code = (detail as Record<string, unknown>).code;
return typeof code === "string" ? code : null;
}
return null;
}Le numéro de la requête voyage dans l'en-tête X-Request-Id, lisible depuis
erreur.headers.
#Deux refus différents portent le code 429
Un 429 ne veut pas dire la même chose selon ce qui l'a produit, et l'en-tête vous dit lequel vous avez.
| Refus | En-têtes | Quand réessayer |
|---|---|---|
| Plafond de débit | Retry-After, plus X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset et X-RateLimit-Scope | après le nombre de secondes donné par Retry-After |
| Quota quotidien de la clef | X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset. Aucun Retry-After | après minuit UTC, quand le quota repart |
Le quota quotidien ne peut vous refuser que sur deux routes, celles qui
consomment des articles : products.mint() et la déclaration de vente. Le
plafond de débit, lui, s'applique à toutes les routes qui lisent votre clef.
X-Quota-Reset porte l'horodatage de la dernière remise à zéro, il ne donne pas
la prochaine.
Réessayer tout de suite après un refus de quota échoue de la même façon. La
fonction ci-dessous fait donc attendre le premier cas, et rend false pour le
second.
import { SealTrustError } from "@sealtrust-io/sdk";
async function attendreSiPlafondDeDebit(erreur: unknown): Promise<boolean> {
if (!(erreur instanceof SealTrustError) || erreur.status !== 429) {
return false;
}
const retryAfter = erreur.headers.get("retry-after");
if (retryAfter === null) {
console.error(
"Quota quotidien atteint :",
erreur.headers.get("x-quota-remaining"),
"sur",
erreur.headers.get("x-quota-limit"),
"Il repart à minuit UTC.",
);
return false;
}
const secondes = Number(retryAfter);
await new Promise((resoudre) => setTimeout(resoudre, secondes * 1000));
return true;
}#Exemple complet de bout en bout
Ce programme fait deux choses à la suite : il déclare un abonnement aux
notifications si vous n'en avez pas encore un sur cette adresse, puis il lit
l'historique d'un produit. Il compile et s'exécute tel quel avec Node 18 ou plus
récent et @types/node installé, une fois la variable SEALTRUST_API_KEY posée
dans votre environnement, et l'adresse comme l'identifiant de produit remplacés
par les vôtres.
La déclaration d'abonnement demande une offre qui comprend les notifications. Si
votre offre ne les comprend pas, l'appel répond 403 FEATURE_NOT_AVAILABLE, le
programme le dit et continue.
import {
SealTrustClient,
SealTrustError,
SealTrustNetworkError,
} from "@sealtrust-io/sdk";
const CLEF = process.env.SEALTRUST_API_KEY ?? "";
const ADRESSE_NOTIFICATION = "https://exemple-sas.example/webhooks/sealtrust";
const SECRET_NOTIFICATION = "secret-de-demonstration-a-remplacer";
const PRODUIT = "EXEMP1E00001";
const sealtrust = new SealTrustClient({ apiKey: CLEF });
function codeDErreur(erreur: SealTrustError): string | null {
const detail = erreur.body?.detail;
if (detail && typeof detail === "object" && !Array.isArray(detail)) {
const code = (detail as Record<string, unknown>).code;
return typeof code === "string" ? code : null;
}
return null;
}
async function chercherAbonnement(): Promise<number | null> {
let vus = 0;
for (;;) {
const page = await sealtrust.webhooks.list({ skip: vus, limit: 100 });
const trouve = page.items.find((item) => item.url === ADRESSE_NOTIFICATION);
if (trouve) {
return trouve.id;
}
vus += page.items.length;
if (page.items.length === 0 || vus >= page.total) {
return null;
}
}
}
async function declarerAbonnement(): Promise<number | null> {
const existant = await chercherAbonnement();
if (existant !== null) {
console.log("Abonnement déjà présent :", existant);
return existant;
}
try {
const cree = await sealtrust.webhooks.create({
url: ADRESSE_NOTIFICATION,
events: ["product.minted", "product.transferred"],
secret: SECRET_NOTIFICATION,
});
console.log("Abonnement créé :", cree.id, cree.events.join(", "));
return cree.id;
} catch (erreur) {
if (
erreur instanceof SealTrustError &&
codeDErreur(erreur) === "FEATURE_NOT_AVAILABLE"
) {
console.error("Offre sans notifications : abonnement non déclaré.");
return null;
}
throw erreur;
}
}
async function lireHistorique(): Promise<void> {
const histoire = await sealtrust.verify.timeline(PRODUIT);
console.log("Produit :", histoire.product_name);
console.log("Marque :", histoire.brand_name);
console.log("Événements :", histoire.timeline.length);
for (const evenement of histoire.timeline) {
console.log(" ", evenement.type, evenement.timestamp);
}
}
async function principal(): Promise<void> {
await declarerAbonnement();
await lireHistorique();
}
principal().catch((erreur) => {
if (erreur instanceof SealTrustError) {
console.error(`L'API a refusé en ${erreur.status} :`, erreur.message);
console.error("Numéro de requête :", erreur.headers.get("x-request-id"));
} else if (erreur instanceof SealTrustNetworkError) {
console.error("API injoignable :", erreur.message);
} else {
console.error(erreur);
}
process.exitCode = 1;
});La lecture de la liste avant la création est délibérée. Sans elle, un programme relancé après une coupure déclare un second abonnement sur la même adresse, et votre serveur récepteur reçoit chaque événement deux fois.
#Les pièges connus, en un coup d'œil
| Ce que vous voyez | Cause | Ce qu'il faut faire |
|---|---|---|
un lot finished sans savoir combien d'articles ont abouti | ce statut décrit la file d'exécution, il ne compte pas les articles | ouvrez la liste des produits dans la console, le SDK n'expose aucune méthode qui la lise |
| 400 sur un lot que le compilateur accepte | vous envoyez owner_email ou contract_address, encore déclarés dans le type et refusés par l'API | retirez ces deux champs |
401 sur verify.batch() ou verify.metadataIntegrity() | ces routes attendent une session de la console | n'utilisez pas ces deux méthodes avec une clef d'API |
| 403 avec des droits nommés dans le message | la clef n'a pas le droit demandé par la route | recréez une clef portant le droit nommé, depuis la console |
| 403 sur tout le lot | une ligne au moins porte un brand_id différent de celui de la clef | corrigez la ligne fautive, le lot entier est refusé pour une seule |
403 FEATURE_NOT_AVAILABLE | votre offre ne comprend pas la fonction appelée : l'accès à l'API, ou les notifications sur une création et sur une modification d'abonnement | vérifiez votre offre dans la console |
403 AUTH_METHOD_NOT_ALLOWED sur un lot | votre offre n'autorise pas la méthode qr, que le serveur pose sur chaque ligne de ce point d'entrée | vérifiez votre offre dans la console |
403 QUOTA_EXCEEDED | le forfait mensuel de produits de votre marque est atteint, et ce lot le dépasserait | réduisez la taille du lot ou attendez la période suivante ; le corps donne current, additional, max et period |
products.list is not a function, ou sealtrust.certificates vaut undefined | vous appelez une méthode absente du SDK : products.list(), products.get(), verify.product(), ou la famille certificates | aucune version publiée du paquet ne les a portées, et les routes correspondantes n'ont jamais existé |
undefined en lisant hook.events sur un élément de liste | la liste rend event_types | lisez item.event_types sur les éléments de list() |
| chaque événement livré deux fois | deux abonnements portent la même adresse, un create() a été rejoué | listez vos abonnements et supprimez le doublon |
| une boucle d'attente qui ne s'arrête jamais | vous testez is_finished, absent de la réponse unknown | bouclez sur status, et arrêtez-vous dès qu'il quitte queued et started |
400 CONFIRMATION_MISMATCH à la suppression | l'adresse envoyée ne correspond pas à celle enregistrée sous cet identifiant | relisez l'abonnement avec webhooks.get(id) et passez son url |
429 avec Retry-After | le plafond de débit est atteint | attendez le nombre de secondes donné par Retry-After |
429 sans Retry-After | le quota quotidien de la clef est atteint | attendez minuit UTC ; réessayer avant échoue de la même façon |
| 503 | le service de plafonnement est momentanément indisponible | réessayez peu après, rien n'a été traité |
Cette page vous a-t-elle été utile ?
Votre réponse ouvre un courriel pré-rempli dans votre messagerie, à destination de contact@sealtrust.io. Vous le relisez avant de l'envoyer.