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

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éthodeRoute appeléeDroit requis sur la clef
products.mint()POST /partner/mint/batchmint:batch
products.getBatchStatus()GET /partner/mint/batch/status/{job_id}mint:batch
webhooks.create()POST /partner/webhookswebhooks:write
webhooks.list()GET /partner/webhookswebhooks: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.

Terminal
npm install @sealtrust-io/sdk
Terminal
yarn add @sealtrust-io/sdk

Le 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.

Terminal
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.

TypeScript
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.

OptionTypeObligatoireDescription
apiKeystringouiVotre 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.
baseUrlstringnonL'adresse de l'API. Valeur par défaut https://api.sealtrust.io.
timeoutnumbernonDélai maximal d'un appel, en millisecondes. Valeur par défaut 30000.
fetchtypeof fetchnonL'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.

TypeScript
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, 0x suivi de 64 caractères hexadécimaux.
TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

ChampTypeObligatoireDescription
product_namestringouiLe nom du produit.
brand_idnumberouiLe numéro de votre marque. Il doit correspondre à la marque de la clef.
category_idnumberouiLe numéro de la catégorie.
metadata_uristringouiL'adresse des métadonnées du produit.
external_refstringnonVotre 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.

TypeScript
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 code AUTH_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 donne current, additional, max et period.
  • 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.

TypeScript
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.

TypeScript
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.

ClasseCe qui s'est passéCe que vous avez
SealTrustErrorL'API a répondu et a refusé.status, body, headers.
SealTrustNetworkErrorL'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.

TypeScript
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.

TypeScript
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.

RefusEn-têtesQuand réessayer
Plafond de débitRetry-After, plus X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset et X-RateLimit-Scopeaprès le nombre de secondes donné par Retry-After
Quota quotidien de la clefX-Quota-Limit, X-Quota-Remaining, X-Quota-Reset. Aucun Retry-Afteraprè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.

TypeScript
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.

TypeScript
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 voyezCauseCe qu'il faut faire
un lot finished sans savoir combien d'articles ont aboutice statut décrit la file d'exécution, il ne compte pas les articlesouvrez la liste des produits dans la console, le SDK n'expose aucune méthode qui la lise
400 sur un lot que le compilateur acceptevous envoyez owner_email ou contract_address, encore déclarés dans le type et refusés par l'APIretirez ces deux champs
401 sur verify.batch() ou verify.metadataIntegrity()ces routes attendent une session de la consolen'utilisez pas ces deux méthodes avec une clef d'API
403 avec des droits nommés dans le messagela clef n'a pas le droit demandé par la routerecréez une clef portant le droit nommé, depuis la console
403 sur tout le lotune ligne au moins porte un brand_id différent de celui de la clefcorrigez la ligne fautive, le lot entier est refusé pour une seule
403 FEATURE_NOT_AVAILABLEvotre 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'abonnementvérifiez votre offre dans la console
403 AUTH_METHOD_NOT_ALLOWED sur un lotvotre offre n'autorise pas la méthode qr, que le serveur pose sur chaque ligne de ce point d'entréevérifiez votre offre dans la console
403 QUOTA_EXCEEDEDle forfait mensuel de produits de votre marque est atteint, et ce lot le dépasseraitré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 undefinedvous appelez une méthode absente du SDK : products.list(), products.get(), verify.product(), ou la famille certificatesaucune 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 listela liste rend event_typeslisez item.event_types sur les éléments de list()
chaque événement livré deux foisdeux 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 jamaisvous testez is_finished, absent de la réponse unknownbouclez sur status, et arrêtez-vous dès qu'il quitte queued et started
400 CONFIRMATION_MISMATCH à la suppressionl'adresse envoyée ne correspond pas à celle enregistrée sous cet identifiantrelisez l'abonnement avec webhooks.get(id) et passez son url
429 avec Retry-Afterle plafond de débit est atteintattendez le nombre de secondes donné par Retry-After
429 sans Retry-Afterle quota quotidien de la clef est atteintattendez minuit UTC ; réessayer avant échoue de la même façon
503le service de plafonnement est momentanément indisponibleréessayez peu après, rien n'a été traité

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