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

Source : https://docs.sealtrust.io/sdk-typescript/

---

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.

> [!DANGER] Votre clef d'API ouvre l'accès à vos produits
> Gardez-la sur votre serveur. Ne la posez ni dans un dépôt de code, ni dans une
> page de navigateur, ni dans une application mobile : toute personne qui la lit
> appelle l'API à votre place. Lisez-la depuis une variable d'environnement,
> comme le font tous les exemples de cette page.

## 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`](/reference/post-partner-mint-batch/) | `mint:batch` |
| `products.getBatchStatus()` | [`GET /partner/mint/batch/status/{job_id}`](/reference/get-partner-mint-batch-status/) | `mint:batch` |
| `webhooks.create()` | [`POST /partner/webhooks`](/reference/post-partner-webhooks/) | `webhooks:write` |
| `webhooks.list()` | [`GET /partner/webhooks`](/reference/get-partner-webhooks/) | `webhooks:read` |
| `webhooks.get()` | [`GET /partner/webhooks/{webhook_id}`](/reference/get-partner-webhooks-id/) | `webhooks:read` |
| `webhooks.update()` | [`PUT /partner/webhooks/{webhook_id}`](/reference/put-partner-webhooks-id/) | `webhooks:write` |
| `webhooks.delete()` | [`DELETE /partner/webhooks/{webhook_id}`](/reference/delete-partner-webhooks-id/) | `webhooks:write` |
| `verify.timeline()` | [`GET /timeline/{identifier}`](/reference/get-timeline/) | 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`](/reference/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.

```bash
npm install @sealtrust-io/sdk
```

```bash
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.

```bash
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.

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

> [!ATTENTION] `baseUrl` prend un hôte, sans chemin
> Chaque méthode envoie déjà un chemin qui commence par `/v1/`. Si vous
> renseignez `baseUrl` avec un chemin, ce chemin est remplacé au lieu d'être
> ajouté. Écrivez `https://api.sealtrust.io` et rien de plus.

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.

> [!ATTENTION] La création et la modification demandent une offre qui comprend les notifications
> `webhooks.create()` et `webhooks.update()` répondent 403 avec le code
> `FEATURE_NOT_AVAILABLE` quand l'offre de votre marque ne comprend pas les
> notifications. L'offre d'entrée de gamme qui ouvre l'accès à l'API est dans ce
> cas. Une seule modification échappe à la règle, l'extinction seule, c'est-à-dire
> un corps qui ne porte que `{ is_active: false }`. La lecture, la liste et la
> suppression restent ouvertes sur toutes les offres. Sur une offre sans
> notifications, l'envoi est fermé lui aussi : un abonnement déjà déclaré ne
> reçoit plus rien.

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.

> [!DANGER] Un `webhooks.create()` rejoué crée un second abonnement
> Cette route ne lit aucune clef d'idempotence. Deux appels identiques donnent
> deux abonnements sur la même adresse, et votre serveur récepteur reçoit alors
> chaque événement deux fois. Rien ne vous prévient. Relisez la liste avec
> `webhooks.list()` avant de créer, et créez seulement si l'adresse est absente.

> [!ATTENTION] La liste et la lecture unitaire ne rendent pas la même forme
> `webhooks.get()`, `webhooks.create()` et `webhooks.update()` rendent le champ
> sous le nom `events`. Les éléments de `webhooks.list()` le rendent sous le nom
> `event_types`. Cet écart vient de l'API. Le SDK le laisse visible. S'il le
> masquait, vous liriez un tableau qui n'arrive jamais.

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

> [!ATTENTION] Ce chemin crée des articles identifiés par QR code
> Un appel machine n'a aucune puce NFC en main, et rien ici ne permet de
> rattacher l'identifiant d'une puce à un article. Les articles NFC se créent
> depuis la console, au moment où la puce est encodée. Le QR est un mode
> d'identification de plein droit.

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

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

> [!DANGER] Le type autorise deux champs que l'API refuse
> Le type `ProductMintRequest` de la version `0.3.0` déclare encore
> `owner_email` et `contract_address`. Ces deux champs ont été retirés de l'API
> le 20 août 2026. Les envoyer fait échouer la requête en 400, alors que votre
> code compile sans une remarque. Ne les renseignez pas.

### 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`.

> [!ATTENTION] `finished` décrit la file
> Ce champ dit que la file a fini de traiter le lot. Il ne dit rien sur le
> résultat. Un lot dont chaque ligne a été rejetée répond `finished` lui aussi.
> Pour connaître le résultat article par article, ouvrez la liste des produits
> dans la console de votre marque. Le SDK n'expose aucune méthode qui la lise.

> [!ATTENTION] `unknown` veut dire introuvable
> Cette réponse ne porte que `job_id` et `status`. Un `is_finished` absent
> signifie donc que le lot est introuvable. N'en concluez pas que le lot est
> encore en cours, votre boucle ne s'arrêterait jamais.

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.

> [!DANGER] Deux champs de diagnostic à ignorer
> Le type de réponse déclare deux champs destinés au diagnostic interne. Leur
> contenu n'est pas un contrat, il peut changer sans préavis et il ne décrit pas
> le résultat article par article. Ne construisez aucune logique dessus.

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

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

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

```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 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é |
