# Remplir le catalogue par l'API

Créer ou compléter des modèles, des lots et des passeports brouillons depuis votre plateforme, lire le score de complétude, et agir pour plusieurs marques avec une clef revendeur. Droits catalog:read et catalog:write.

Source : https://docs.sealtrust.io/api-catalogue/

---

Votre plateforme connaît déjà vos produits. Cette page explique comment les
envoyer dans SealTrust sans que personne ne les ressaisisse : les modèles, les
lots de production et le brouillon du passeport de chaque modèle. Vous envoyez
ce que vous avez. La marque complète le reste dans la console, puis publie.

## Ce qu'il vous faut

- Une clef d'API qui porte le droit `catalog:write` pour écrire, et
  `catalog:read` pour relire. Vous la créez dans la console, Paramètres puis
  Développeurs.
- Une offre qui comprend l'accès API. Pour les passeports, l'offre de la marque
  visée doit aussi comprendre le passeport numérique.

## Les dix points d'entrée

Chaque adresse nomme la marque visée dans son chemin. Toutes existent aussi avec
le préfixe `/v1`, qui est la forme recommandée.

| Méthode et chemin | Ce qu'il fait | Droit |
| --- | --- | --- |
| `GET /partner/catalog/brands` | les marques pour lesquelles votre clef peut agir | `catalog:read` ou `catalog:write` |
| `PUT /partner/catalog/brands/{brand_code}/models` | crée ou complète un modèle, repéré par son SKU | `catalog:write` |
| `GET /partner/catalog/brands/{brand_code}/models` | un modèle avec `?sku=`, sinon la liste des modèles | `catalog:read` |
| `GET /partner/catalog/brands/{brand_code}/readiness?sku=` | le score de complétude d'un modèle | `catalog:read` |
| `PUT /partner/catalog/brands/{brand_code}/batches` | crée ou complète un lot, repéré par son code | `catalog:write` |
| `PUT /partner/catalog/brands/{brand_code}/passports` | crée ou complète le passeport brouillon d'un modèle, ou d'un de ses lots avec `batch_code` | `catalog:write` |
| `GET /partner/catalog/brands/{brand_code}/passports?sku=` | la dernière version du passeport de ce modèle, ou de l'un de ses lots avec `&batch_code=` | `catalog:read` |
| `GET /partner/catalog/brands/{brand_code}/offer` | l'offre de la marque : Conformité, ou Conformité + Identité | `catalog:read` ou `catalog:write` |
| `PUT /partner/catalog/brands/{brand_code}/offer` | change l'offre d'une de vos marques clientes | `catalog:write` |
| `GET /partner/catalog/brands/{brand_code}/codes?sku=` | le lien et le QR code à imprimer, pour un modèle ou un lot | `catalog:read` |

Le SKU passe dans le corps ou dans la requête, jamais dans le chemin : un SKU
peut contenir une barre oblique.

## Nommer la marque

Chaque marque a un code public de dix caractères : `brand_code`, par exemple
`7K3QXW9M2A`. Ce code est fait de chiffres et de lettres, sans I, L, O ni U.
Les majuscules et les minuscules se valent. `GET /partner/catalog/brands` donne
le code de chaque marque pour laquelle votre clef peut agir. Mettez ce code
dans le chemin.

Les réponses portent aussi `brand_id`, le numéro interne de la marque. Ce champ
est déprécié : lisez `brand_code`. Un numéro envoyé à la place du code est
encore accepté pour l'instant, mais ne construisez rien dessus.

## Quatre règles valent pour chaque écriture

**Rejouer ne change rien.** Le SKU d'un modèle et le code d'un lot servent de
clef. Envoyer deux fois la même chose laisse le même état. La deuxième réponse
porte `changed: false`. Après une coupure réseau, rejouez sans crainte : aucun
en-tête `Idempotency-Key` n'est nécessaire, et aucun doublon n'est possible.
Cela vaut aussi quand votre nouvel essai arrive alors que le premier appel
tourne encore : un seul brouillon est ouvert.

**Un champ absent n'efface rien, une liste envoyée remplace la liste.** Un champ
absent, ou envoyé à `null`, garde sa valeur, et un objet se complète clé par
clé : ce que la marque a saisi dans les autres clés reste en place. Une liste,
elle, est remplacée en entier. Si la marque a ajouté un fournisseur dans la
console et que votre envoi suivant contient `suppliers`, c'est votre liste qui
reste, et l'ajout de la marque disparaît. Avant de renvoyer une liste, relisez
le brouillon par `GET /partner/catalog/brands/{brand_code}/passports?sku=` et
renvoyez la liste complète, ou n'envoyez pas la liste. Pour vider un champ,
passez par la console.

**Rien n'est publié, rien de scellé n'est réécrit.** Un passeport envoyé par
l'API reste un brouillon privé. La publication reste un geste de la marque,
dans la console. Si la marque a déjà scellé une version, votre envoi suivant
ouvre une nouvelle version brouillon par-dessus : la version scellée ne bouge
jamais.

**Chaque écriture laisse une trace.** SealTrust inscrit chaque écriture dans
son journal d'audit, comme pour une modification faite dans la console. Le
journal note la clef qui a écrit, la marque pour laquelle elle a agi, et chaque
champ modifié avec sa valeur avant et après. L'écriture d'un modèle apparaît
aussi dans l'historique de ce modèle dans la console, avec l'origine « API » et
le préfixe de la clef pour auteur. Une écriture qui ne change rien n'y laisse
rien. Chaque appel, lecture comprise, est aussi inscrit au journal
d'utilisation de la clef, avec la marque visée dans son chemin.

## Créer ou compléter un modèle

Le corps accepte les champs d'un modèle de la console. `sku` est obligatoire.
`name` n'est obligatoire qu'à la création. Un GTIN dont la clef de contrôle est
fausse est refusé en 422, et tout champ inconnu aussi.

`primary_image_url` est l'image que nous recopions sur IPFS à la création des pièces. Elle doit être une adresse `http` ou `https` vers un serveur public. Une adresse de réseau privé, locale ou sans nom de domaine complet est refusée en 422. Si le nom pointe vers une adresse privée au moment de la création des pièces, l'image n'est pas recopiée et la pièce est créée quand même.

`primary_image_url` peut aussi être une clé de la médiathèque de la marque
visée, de la forme `brands/{id}/…`. Une clé d'une autre marque est refusée en
422. Toute valeur qui n'est ni une adresse ni une clé, par exemple
`javascript:alert(1)` ou un texte libre, est refusée en 422 aussi.

`weight_grams`, `volume_cm3` et `default_warranty_months` valent 0 ou plus.
Une valeur négative est refusée en 422.

Les six liens d'instructions (`repair_instructions_url`,
`maintenance_instructions_url`, `safety_instructions_url`, `user_manual_url`,
`spare_parts_url`, `disassembly_instructions_url`) sont des adresses web
complètes, qui commencent par `https://` ou `http://`. Un chemin relatif, un
texte libre, une adresse `javascript:` ou `data:` sont refusés en 422.

`identity_level` vaut `unit` : chaque pièce reçoit son propre jeton et son
propre QR code, pour l'authentification, le propriétaire, la revente et la
déclaration de vol, et sa page affiche le passeport de son lot, sinon celui du
modèle. Pour qu'une pièce frappée par `POST /v1/partner/mint/batch` soit
rattachée ainsi, nommez sur sa ligne `model_sku`, et `batch_code` si elle
appartient à un lot. Un modèle créé sans ce champ est au niveau `unit`, comme par tout autre
chemin. Envoyer `model` répond `422`. Un envoi sans ce champ ne change jamais le
niveau d'un modèle existant.

```bash title="curl"
curl -X PUT https://api.sealtrust.io/v1/partner/catalog/brands/7K3QXW9M2A/models \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"sku": "BAG-01", "name": "Canvas tote", "gtin": "4006381333931"}'
```

La réponse vaut 201 à la création, 200 sinon.

```json
{
  "created": true,
  "changed": true,
  "model": {
    "id": 301,
    "brand_code": "7K3QXW9M2A",
    "sku": "BAG-01",
    "name": "Canvas tote",
    "gtin": "4006381333931"
  }
}
```

Le modèle rendu porte tous ses champs. Cet exemple n'en montre que cinq.

Le gabarit du passeport, et donc le score de complétude, se déduit de la
catégorie du modèle (`category_id`). Pour une batterie, choisissez la catégorie
« Battery » : le modèle est alors noté contre le règlement (UE) 2023/1542 sur les
batteries, et non contre l'électronique grand public. Vous pouvez aussi nommer le
gabarit vous-même avec `playbook_key`, par exemple `"playbook_key": "battery"`.
Ce champ prime sur la catégorie. Une valeur inconnue est refusée en 422.

## Créer ou compléter un lot

`batch_code` et `model_sku` sont obligatoires. Le SKU désigne un modèle de la
même marque. Les autres champs sont `production_date` (date au format
`AAAA-MM-JJ`), `manufacturing_site`, `country_of_origin` (deux lettres),
`quantity_planned` et `notes`.

```bash title="curl"
curl -X PUT https://api.sealtrust.io/v1/partner/catalog/brands/7K3QXW9M2A/batches \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"batch_code": "L-2026-09", "model_sku": "BAG-01", "country_of_origin": "FR"}'
```

Deux refus protègent ce qui est déjà établi. Un lot ne change pas de modèle
par l'API. Un lot ancré sur la chaîne ne change plus du tout : ses faits sont
publiés.

Le lot rendu porte `lot_passport_blocker`. Il vaut `null` quand le code de lot
peut porter un passeport de lot. Sinon, il dit pourquoi : le lien
`/01/{gtin}/10/{lot}` exige un code GS1 de 20 caractères au plus, pris dans un
jeu restreint (lettres, chiffres et quelques signes, sans espace ni barre oblique). Le lot est
créé quand même, car son code est aussi votre référence de production. Il
n'aura simplement jamais de passeport de lot : choisissez un code plus court si
vous en voulez un.

## Créer ou compléter le passeport brouillon

`sku` nomme le modèle. `data` suit la structure du passeport de la console.
Nous fusionnons `data` dans la dernière version : les objets se complètent clef
par clef, le reste remplace, et `null` garde la valeur en place.

```bash title="curl"
curl -X PUT https://api.sealtrust.io/v1/partner/catalog/brands/7K3QXW9M2A/passports \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"sku": "BAG-01", "data": {"product_identity": {"name": "Canvas tote"}}}'
```

La réponse porte le passeport, avec `status` à `draft`, et
`visibility` à `brand_only`. Il n'existe aucun champ pour publier : l'envoyer
est refusé en 422.

### Le passeport d'un lot

Ajoutez `batch_code` pour écrire le passeport d'un lot de ce modèle plutôt que
celui du modèle. C'est lui qui répond au lien `/01/{gtin}/10/{lot}`. Une
première version part du passeport publié du modèle, complété des informations
du lot. Le GTIN et le numéro du lot y sont toujours écrits : des données qui
nomment un autre lot sont refusées en 422, jamais écrasées. Le lot doit
appartenir au modèle nommé par `sku`, sinon la réponse est 404. Pour relire ce
passeport, ajoutez `&batch_code=` à la lecture.

```bash title="curl"
curl -X PUT https://api.sealtrust.io/v1/partner/catalog/brands/7K3QXW9M2A/passports \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"sku": "BAG-01", "batch_code": "LOT-26A", "data": {"materials": {"cotton": 100}}}'
```

La réponse porte `batch_code` et `product_batch_id`. `product_model_id` y vaut
`null` : un passeport de lot est rattaché à son lot.

## L'offre d'une marque

Chaque marque est sur l'offre Conformité (`compliance`) ou Conformité +
Identité (`compliance_identity`). La liste des marques porte l'offre de chacune.
`GET .../offer` la relit.

Avec une clef revendeur, `PUT .../offer` change l'offre d'une de vos marques
clientes, avec les mêmes règles que la console :

```bash title="curl"
curl -X PUT https://api.sealtrust.io/v1/partner/catalog/brands/7K3QXW9M2A/offer \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"offer": "compliance"}'
```

- Une marque ne change jamais sa propre offre, un revendeur non plus : la
  réponse est le même 403 que pour une marque hors de portée de la clef.
- Quitter l'offre Conformité + Identité alors que la marque a des pièces, ou
  des lots en cours de création, répond 409 `OFFER_CHANGE_NEEDS_CONFIRMATION`,
  avec `pieces` et `batches_in_flight`. Les pièces sont gardées, mais leurs
  opérations d'identité s'arrêtent, et un lot en cours ne crée aucune pièce.
  Renvoyez avec `"confirm_pieces_kept": true`.
- Une offre inconnue répond 422 `UNKNOWN_OFFER`.
- La réponse dit `changed` (faux si la marque avait déjà cette offre) et
  `pieces_kept`. Le changement est inscrit au journal, au nom de la clef.

Le détail des deux offres est sur [la page des offres](/offres-conformite-et-identite/).

## Le code à imprimer

Une marque sur l'offre Conformité imprime le même code sur chaque produit d'un
modèle, ou d'un lot. `GET .../codes?sku=` le donne pour un modèle,
`&batch_code=` pour un lot :

```bash title="curl"
curl "https://api.sealtrust.io/v1/partner/catalog/brands/7K3QXW9M2A/codes?sku=BAG-01&batch_code=LOT-26A" \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000"
```

- `link` : le lien GS1 que porte le code, sur le domaine de la marque une fois
  celui-ci verrouillé.
- `printable` : vrai quand le code peut être imprimé. `qr_png` et
  `qr_png_download` donnent alors l'adresse de l'image, à ouvrir sans clef.
- Sinon `reason` dit pourquoi : `ITEM_QR_ONLY` (la marque est sur l'offre
  Conformité + Identité, chaque pièce porte son propre code),
  `PASSPORT_NOT_PUBLISHED` (la marque publie d'abord ce passeport dans la
  console), `MODEL_HAS_NO_GTIN` ou `LOT_CODE_NOT_ENCODABLE` (aucun lien GS1
  possible).

## Lire le score de complétude

C'est le score que la console affiche, de 0 à 100, avec le détail de ce qui
manque.

```bash title="curl"
curl "https://api.sealtrust.io/v1/partner/catalog/brands/7K3QXW9M2A/readiness?sku=BAG-01" \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000"
```

## Une clef pour plusieurs marques : la clef revendeur

Une clef ordinaire agit pour sa marque, et pour elle seule.

Une **clef revendeur** agit pour la marque du revendeur et pour chacune des
marques clientes rattachées à son contrat revendeur. Vous nommez la marque
visée à chaque appel, dans le chemin. `GET /partner/catalog/brands` rend la
liste des marques permises, la vôtre en premier.

Elle fait pour une marque cliente tout ce que votre contrat vous permet : le
catalogue, l'offre, les codes à imprimer,
[l'affichage du passeport](/api-affichage-partenaire/), la frappe des pièces,
la déclaration de vente et les webhooks.

Ce que la clef revendeur ne fait pas :

- Elle n'atteint aucune marque qui n'est pas rattachée à votre contrat. Une
  marque d'un autre revendeur, une marque sans lien, ou un code qui ne nomme
  aucune marque reçoivent tous le même 403.
- Elle suit votre contrat, relu à chaque appel. Sans contrat, elle n'atteint
  plus que votre propre marque.

La case « Clé revendeur » se coche à la création de la clef, dans la console.
Elle est refusée pour une marque qui n'a pas de contrat revendeur.

### Frapper, vendre et recevoir des webhooks pour une marque cliente

Avec votre clé revendeur, vous frappez les pièces d'une marque cliente sans
clé de plus : mettez le code de la marque cliente dans le `brand_code` de
chaque ligne de `POST /v1/partner/mint/batch`. Toutes les lignes d'un lot
portent la même marque. Un lot qui en mélange plusieurs est refusé en 422
`ONE_BRAND_PER_BATCH` : envoyez un lot par marque. Le suivi du lot et la
liste de ses pièces, avec le lien à imprimer, se lisent avec la même clé.

Pour déclarer une vente, ajoutez `brand_code` au corps de
`POST /v1/partner/sellout`. Pour un webhook, ajoutez `brand_code` au corps de
`POST /v1/partner/webhooks`, et à la liste :
`GET /v1/partner/webhooks?brand_code=7K3QXW9M2A`. Sans `brand_code`, la clé
agit pour votre propre marque. N'envoyez pas `brand_code` et `brand_id`
ensemble : la requête est refusée en 422.

Ce sont les règles de la marque cliente qui s'appliquent : son offre (en offre
Conformité, la marque n'a pas de pièces et la frappe est refusée), ses pièces
de l'année et son droit aux webhooks. L'accès à l'API vient de votre offre de
revendeur. Chaque écriture est tracée avec la marque visée et votre clé.

Un webhook que vous créez pour une marque cliente, par l'API ou dans la
console, n'est envoyé que tant que votre contrat de revendeur existe et que la
marque est toujours votre cliente. Quand l'un des deux s'arrête, votre adresse
ne reçoit plus rien de cette marque, relances comprises. Changer de clé ne
change rien : vos webhooks continuent d'arriver.

Une clé créée sur la marque cliente elle-même, dans la console, fonctionne
toujours, pour cette marque seule.

## Plafonds

Le plafond de débit est celui de toute l'API partenaire, décrit dans la
[vue d'ensemble](/api-vue-ensemble/). Le quota quotidien de la clef compte une
unité par écriture qui crée ou modifie quelque chose. Une écriture rejouée qui
ne change rien ne coûte rien.

## Erreurs

| Code | Condition | Que faire |
| --- | --- | --- |
| 401 | Clef absente, mal formée ou inconnue. | Envoyez `Authorization: Bearer <votre clef>`. |
| 403 | Le droit manque. Le message le nomme, par exemple `Missing required scope: catalog:write`. | Créez une clef qui porte ce droit. |
| 403 | Votre clef ne peut pas agir pour cette marque. Message `This API key cannot act for this brand.` | Vérifiez le code de la marque avec `GET /partner/catalog/brands`. |
| 403 | L'offre ne comprend pas l'accès API, ou pas le passeport numérique. `detail` porte `FEATURE_NOT_AVAILABLE`. | Contactez-nous pour changer d'offre. |
| 403 | La création d'un modèle ou d'un lot est suspendue pour une facture mensuelle impayée. `detail` porte `BILLING_SUSPENDED`. Les passeports déjà publiés restent en ligne, et la mise à jour d'un modèle ou d'un lot existant reste possible. | Réglez la facture. La suspension est levée dès le paiement. |
| 404 | Aucun modèle de cette marque ne porte ce SKU, ou le modèle n'a pas encore de passeport. | Créez d'abord le modèle. |
| 404 | `category_id` inconnu. | Utilisez un identifiant de catégorie existant. |
| 409 | Le lot appartient à un autre modèle, ou il est ancré sur la chaîne. | Faites ce changement dans la console. |
| 409 | Le nouveau GTIN est déjà publié par une autre marque. | Vérifiez le GTIN. |
| 409 | Un lot de ce modèle a un passeport de lot publié : le GTIN est imprimé dans son lien GS1 et scellé dans ses données, il ne change plus. | Gardez le GTIN actuel. |
| 409 | Deux écritures ont ouvert une version du même passeport au même instant, deux fois de suite. Cet appel n'a rien écrit. | Renvoyez la requête. |
| 409 | `OFFER_CHANGE_NEEDS_CONFIRMATION` : la marque a des pièces, ou des lots en cours de création. | Renvoyez avec `"confirm_pieces_kept": true` si c'est voulu. |
| 422 | `UNKNOWN_OFFER` : l'offre demandée n'existe pas. | Envoyez `compliance` ou `compliance_identity`. |
| 422 | Corps invalide : champ inconnu, `name` absent à la création, GTIN à la clef fausse, image hors d'un serveur public ou clé d'image d'une autre marque, poids, volume ou garantie négatif, lien d'instructions qui n'est pas une adresse web, pays qui n'est pas deux lettres. | Le corps de la réponse nomme le champ fautif. |
| 429 | Plafond de débit ou quota quotidien atteint. | Attendez le délai indiqué par `Retry-After`, ou minuit en temps universel pour le quota. |
| 503 | Le comptage des appels est momentanément indisponible. Rien n'a été écrit. | Réessayez dans quelques secondes. |

## Voir aussi

- [API partenaire, vue d'ensemble](/api-vue-ensemble/), clefs, droits et
  plafonds.
- [SDK TypeScript](/sdk-typescript/), qui ne couvre pas encore ces points
  d'entrée dans sa version publiée.
- [Clés d'API et webhooks](/console/reglages-developpeurs/), créer la clef dans
  la console.
