# Journal des changements

Ce qui a changé dans l'API partenaire et dans les notifications, par date, avec le geste exact à faire quand une entrée casse un appel qui passait avant.

Source : https://docs.sealtrust.io/journal-des-changements/

---

En quittant cette page, vous saurez ce qui a changé dans l'API partenaire et
dans les notifications, à quelle date, et ce que vous avez à faire de votre
côté quand un changement refuse un appel qui passait avant. Vous saurez aussi
pourquoi le numéro de version que vous appelez ne bouge pas alors que ces
entrées s'accumulent.

Lisez cette page en entier la première fois. Ensuite, revenez-y avant chaque
évolution de votre intégration et arrêtez-vous à la première date que vous
connaissez déjà.

## Comment cette page est faite

Vous lisez les entrées de la plus récente à la plus ancienne. La date portée
par une entrée est celle de la mise en service du changement.

Une entrée commence par **Rupture de compatibilité** quand un appel qui
passait avant est refusé après. C'est le seul cas où vous avez du travail. Les
autres entrées décrivent un comportement qui s'ajoute ou une réponse qui
devient plus précise, et votre intégration continue de fonctionner sans
retouche.

Chaque entrée dit trois choses : ce qui change, la réponse exacte que vous
recevez maintenant, et le geste à faire. Aucune entrée ne renvoie ailleurs
pour le geste à faire.

Cette page couvre tous les points d'entrée que ce site documente : l'API
partenaire à clef, les points d'entrée publics sans clef, les lectures de
passeport authentifiées, les notifications envoyées à vos adresses et le portail
partenaire. Elle ne couvre pas la console, dont les écrans évoluent sans contrat
à tenir envers votre code.

## Pourquoi la version ne bouge pas

Le point d'entrée `GET /v1/version` répond toujours la même chose.

```json
{
  "version": "1.0.0",
  "api_version": "v1"
}
```

Les entrées ci-dessous s'inscrivent toutes dans la version 1. Le jour où une
évolution ne pourra pas y tenir, elle s'ajoutera sous `/v2`, et la version 1
gardera son comportement.

Les points d'entrée métier existent à deux adresses qui appellent le même
code : avec le préfixe `/v1`, et sans aucun préfixe. Appelez
`GET /v1/version` quand vous voulez la version de contrat. C'est le seul point
d'entrée de version que ce site documente.

Les deux adresses appellent le même code, donc elles reçoivent en même temps
tous les changements de comportement listés ici. Une seule entrée fait
exception, celle du 16 août 2026, qui ne concernait que les adresses sans
préfixe.

> [!ATTENTION] Une rupture de compatibilité vous concerne même si vous ne changez rien
> Votre intégration ne se met pas à jour toute seule, et l'API non plus ne
> revient pas en arrière pour elle. Une entrée marquée rupture s'applique à
> votre code tel qu'il est déployé aujourd'hui. Les deux gestes qui reviennent
> le plus souvent sont de retirer un champ d'un envoi et d'ajouter un droit
> sur une clef.

## 26 août 2026

Trois changements. Le premier est une rupture de compatibilité, et il concerne
tout partenaire accrédité.

### Rupture de compatibilité. Le niveau d'accès `value_chain` n'existe plus

Le niveau `value_chain` était accordé à tout partenaire portant **n'importe
quelle** accréditation sur la marque, et il rendait la réunion de ce que voient
le réparateur, le recycleur et le fournisseur amont. Un recycleur accrédité y
lisait donc les données de fabrication et la chaîne d'approvisionnement, que le
niveau `recycler` lui refuse. Cette porte est fermée.

Six niveaux subsistent : `public`, `end_user`, `repairer`, `recycler`,
`upstream` et `authority`. Les trois niveaux de métier sont des publics
distincts : détenir une accréditation n'en ouvre aucune autre.

**La réponse exacte.** `?access_tier=value_chain` répond maintenant **422**, sur
`GET /v1/passport/{identifier}`, sur `GET /v1/passport/01/{gtin}` et sur les
trois points d'entrée d'identité vérifiable. `detail` est la liste de validation
habituelle, chaque entrée portant `loc`, `type` et `msg`, et `loc` nomme
`access_tier`.

**Ce que vous faites.** Demandez le niveau de votre métier, `repairer` ou
`recycler`. Vous recevrez moins de champs qu'avant, et ce sont précisément les
champs qui ne relèvent pas de votre métier.

### Le portail partenaire annonce le métier qu'il a servi

Dans `GET /v1/partner-portal/products/{identifier}`, le champ
`passport.access_tier` valait toujours `value_chain`. Il porte maintenant les
métiers réellement servis, joints par un `+` et rangés par ordre alphabétique :
`recycler`, ou `repairer+recycler` pour un compte qui porte les deux
accréditations sur cette marque. Il vaut `public` quand la marque ne vous en a
reconnu aucune, et le passeport est alors filtré au niveau public.

**Ce que vous faites.** Si votre code compare cette valeur à `value_chain`,
reprenez la comparaison. Et attendez-vous à un contenu plus étroit qu'avant :
il est désormais filtré sur vos seules accréditations.

### L'image publique d'un exemplaire remonte à son modèle

Dans `GET /v1/products/{uid_hash}/public`, le champ `image_url` ne regardait que
les médias attachés directement à l'exemplaire, et valait `null` dès qu'il n'en
avait aucun. Il suit maintenant la même chaîne de repli que le reste du site :
une image de l'exemplaire, puis l'image principale de son modèle, puis les
médias du modèle.

**Ce que vous faites.** Rien. Un champ qui valait `null` peut désormais porter
une adresse.

## 22 août 2026

Trois changements sont partis ensemble. Aucun n'est une rupture de
compatibilité.

### Le certificat public nomme la marque émettrice

Le champ `issuer_name` porte le nom de la marque qui a émis le certificat. Le
serveur le calcule à la lecture, et il vaut `null` quand le certificat n'est
rattaché à aucune marque.

Trois surfaces sont concernées : `GET /v1/certificate/{identifier}`, le PDF
rendu par `GET /v1/certificate/{identifier}/download`, et le bloc `certificate`
de `GET /v1/resolve/{identifier}`.

**Ce que vous faites.** Rien dans vos appels. Si vous avez recopié la valeur de
`issuer_name` dans votre propre base, remplacez-la par ce que rend l'API
aujourd'hui.

### Les refus d'authentification répondent en anglais

La porte d'entrée de l'API rendait ses messages en deux langues selon le chemin
appelé. Les messages qu'elle pose dans `detail` sont désormais en anglais :
`Not authenticated`, `Invalid token`, `Invalid token: missing email`,
`Token has been revoked`, `Invalid JWT token`, `Account disabled`,
`User not found` et `Partner account required (repairer or recycler)`.

Deux refus de `POST /v1/originality/read-sig/verify` suivent la même règle et
valent maintenant `Invalid uid_hex` et `Invalid signature_hex`.

**Ce que vous faites.** Rien, si vous branchez votre code sur le code HTTP,
comme le recommande la page des erreurs. Un client qui compare ces phrases
caractère par caractère doit reprendre ses comparaisons, ou mieux, les
supprimer.

### Le portail partenaire a un plafond d'appels

Les quatre points d'entrée `/partner-portal` acceptent 30 appels par fenêtre de
60 secondes, comptés par adresse d'appel. Les quatre partagent ce compteur, et
la forme `/v1/partner-portal/...` le partage avec la forme sans préfixe.

Chaque réponse acceptée porte `X-RateLimit-Limit`, `X-RateLimit-Remaining` et
`X-RateLimit-Reset`. Un dépassement renvoie 429, avec `detail` valant
`Rate limit exceeded: 30 requests per 60s` et un en-tête `Retry-After` valant
`60`.

**Ce que vous faites.** Espacez vos appels au portail et pilotez votre cadence
sur `X-RateLimit-Remaining`. Un parcours humain, chercher un produit puis
déclarer une intervention, reste très en dessous de ce plafond.

## 21 août 2026

### L'envoi d'un lot crée maintenant des produits

`POST /v1/partner/mint/batch` répondait `200`, son traitement allait jusqu'au
bout, et aucun produit n'était créé. Il manquait à chaque ligne deux valeurs
que le contrat d'envoi ne vous laisse pas fournir, et chaque ligne était
rejetée pour cette raison.

Le serveur pose désormais ces deux valeurs lui-même, sur chaque ligne : la
méthode d'identification `qr` et l'identifiant technique du produit.

**Ce que vous faites.** Rien dans vos envois. Le contrat d'une ligne ne change
pas, et une ligne qui porterait la méthode d'identification ou l'identifiant
technique est refusée en `400`, comme tout champ que le contrat ne nomme pas.

Les produits créés par cette voie sont identifiés par QR. Pour des articles
NFC, passez par la console.

> [!ATTENTION] Le suivi de lot répond `finished` même quand aucun article n'a été créé
> `GET /v1/partner/mint/batch/status/{job_id}` décrit d'abord le déroulement du
> traitement. Le résultat du lot se lit ailleurs. Un lot dont chaque ligne a
> été rejetée va au bout de son traitement et se lit `status: "finished"` avec
> `is_finished: true`, sans aucun décompte. Lisez `success_count` pour savoir
> combien d'articles existent.

## 20 août 2026

Sept changements sont partis ensemble. Les quatre premiers sont des ruptures
de compatibilité.

### Rupture de compatibilité. L'API refuse les champs `owner_email` et `contract_address`

Une ligne de lot qui porte l'un de ces deux champs fait échouer toute la
requête en `400`. La réponse nomme le champ refusé.

L'API acceptait ces deux champs et ne les lisait jamais. La valeur que vous y
mettiez n'avait aucun effet.

Une ligne de lot accepte désormais cinq champs et rien d'autre :
`product_name`, `brand_id`, `category_id` et `metadata_uri`, qui sont
obligatoires, et `external_ref`, qui est facultatif.

**Ce que vous faites.** Retirez ces deux colonnes de vos exports CSV et ces
deux clefs de vos objets JSON. Si vous avez besoin de rattacher une ligne à
une référence de votre côté, utilisez `external_ref` : c'est le seul champ
libre du contrat. Nous le conservons sur la ligne du lot et nous le rendons
dans le résultat que lit `GET /v1/partner/mint/batch/status/{job_id}`. Aucun
champ de produit ne le reprend.

Ce retrait fait passer la validation. Depuis le 21 août 2026, il fait aussi
créer les produits, voir l'entrée de cette date.

### Rupture de compatibilité. Rejouer une clef d'idempotence avec un lot différent répond 409

L'en-tête `Idempotency-Key` reste facultatif et reste mémorisé 24 heures.

Rejouer la même valeur avec le même lot rend toujours la réponse du premier
appel, sans rien refrapper. L'ordre des contrôles a changé le même jour. Le
rejeu consomme désormais un jeton de votre plafond de débit, et l'API relit
votre corps de requête avant de vous rendre la réponse mise en cache. Un rejeu
au-delà du plafond reçoit donc `429`. Un rejeu dont le corps est devenu
illisible reçoit `400`.

Rejouer la même valeur avec un lot différent répond maintenant `409`. L'API ne
met pas votre second lot en file, et la réponse vous le dit.

```json
{
  "detail": "Idempotency-Key 'lot-2026-08-20-a' was already used with a different request body"
}
```

Le même code `409` signale aussi qu'un appel portant cette clef est encore en
cours de traitement. Le message est alors `A request with this
Idempotency-Key is already being processed`. Lisez le message avant de décider
quoi faire : le premier cas vous demande de changer votre clef, le second vous
demande d'attendre la fin du traitement en cours.

L'égalité de deux lots porte sur leur contenu une fois lu. L'ordre des lignes,
l'ordre des colonnes, le choix entre CSV et JSON et les cellules vides n'y
changent rien.

**Ce que vous faites.** Fabriquez une valeur d'`Idempotency-Key` par lot
distinct. Si votre code réutilise une valeur fixe, ou une valeur dérivée de
l'heure à la minute, changez-la maintenant.

### Rupture de compatibilité. Le suivi de lot exige le droit `mint:batch`

`GET /v1/partner/mint/batch/status/{job_id}` exige le droit `mint:batch`, le
même que l'envoi du lot. Une clef qui ne le porte pas reçoit `403`, et le
message nomme le droit manquant.

**Ce que vous faites.** Vérifiez que la clef qui interroge le suivi porte
`mint:batch`. Les droits d'une clef se fixent à sa création et aucun écran ne
les modifie ensuite, donc il vous faut une nouvelle clef.

Vérifiez d'abord votre plafond de clefs, qui ne compte que les clefs actives.
L'offre Essentiel n'en autorise qu'une seule : révoquez l'ancienne, puis créez
la nouvelle, en acceptant la coupure entre les deux. L'offre Prestige en
autorise cinq : créez la nouvelle, déployez-la, puis révoquez l'ancienne.
Révoquer une clef libère immédiatement une place. Une création au-delà du
plafond répond `403` avec le code `QUOTA_EXCEEDED`.

### Rupture de compatibilité. Modifier un abonnement aux notifications exige que votre offre les comprenne

`PUT /v1/partner/webhooks/{webhook_id}` regarde désormais l'offre de votre
marque. Un appel qui passait la veille pour une marque dont l'offre ne
comprend pas les notifications est refusé en `403`.

```json
{
  "detail": {
    "code": "FEATURE_NOT_AVAILABLE",
    "feature": "webhooks"
  }
}
```

Une extinction seule fait exception : envoyer `is_active` à `false` et rien
d'autre reste possible sans l'offre. Dès qu'un autre champ accompagne cette
extinction, l'API exige l'offre.

La création d'un abonnement, `POST /v1/partner/webhooks`, répond la même chose
et l'exigeait déjà avant ce changement.

**Ce que vous faites.** Rien si votre offre comprend les notifications. Si
elle ne les comprend plus, arrêtez de modifier vos abonnements : éteignez-les
avec `is_active` à `false` seul, ou supprimez-les.

### Un corps JSON illisible répond 400

Un corps mal formé envoyé à `POST /v1/partner/mint/batch` répond maintenant
`400`, avec un message qui rappelle les deux formats acceptés.

```json
{
  "detail": "Corps de requete illisible : le JSON envoye est mal forme. Envoyez une liste d'objets produit en application/json, ou un fichier CSV en multipart/form-data."
}
```

**Ce que vous faites.** Si votre code rejoue automatiquement les réponses de
la famille `5xx`, ce cas ne le déclenche plus. Une requête refusée en `400`
est à corriger, et la rejouer telle quelle donnera le même refus.

### Les huit points d'entrée à clef sont plafonnés en débit

Le plafond d'appels s'applique maintenant aux huit points d'entrée de l'API
partenaire, y compris le suivi de lot et les cinq points d'entrée
d'abonnement aux notifications.

Deux conséquences visibles. Au-delà du plafond, vous recevez `429` avec
l'en-tête `Retry-After`, exprimé en secondes. Quand le service de plafonnement
est indisponible, vous recevez `503`, et l'appel n'a aucun effet : une
suppression refusée ainsi ne supprime rien.

Les réponses qui passent portent `X-RateLimit-Limit`, `X-RateLimit-Remaining`,
`X-RateLimit-Reset` et `X-RateLimit-Scope`.

**Ce que vous faites.** Lisez `X-RateLimit-Remaining` et ralentissez avant
d'atteindre zéro. Si vous interrogez le suivi d'un lot en boucle, espacez vos
appels : c'est le point d'entrée le plus exposé à ce plafond. Traitez le `503`
comme une indisponibilité passagère et réessayez après un délai.

### Vos abonnements aux notifications restent lisibles et supprimables quelle que soit votre offre

Lire la liste de vos abonnements, lire un abonnement et supprimer un
abonnement fonctionnent quelle que soit l'offre de votre marque. Ces trois
appels ne regardent pas votre offre.

L'envoi des notifications suit une autre règle : une marque dont l'offre ne
comprend plus les notifications ne reçoit plus de livraison, quels que soient
les abonnements encore enregistrés.

**Ce que vous faites.** Rien. Si votre offre ne comprend plus les
notifications, vous gardez la main pour inventorier vos abonnements et pour
les supprimer.

## 16 août 2026

### Les adresses sans préfixe deviennent des alias permanents

Les copies sans préfixe `/v1` répondaient avec deux en-têtes qui annonçaient
une fin de service au 1er septembre 2026, `Deprecation` et `Sunset`. L'API
n'envoie plus ces deux en-têtes. Les adresses en `/v1` ne les ont jamais
portés.

Aucune date de fin de service n'est attachée à ces adresses. Si votre
intégration les appelle déjà, elle continue de fonctionner.

**Ce que vous faites.** Rien dans l'immédiat. Utilisez la forme `/v1` pour
toute nouvelle intégration : c'est la forme documentée partout sur ce site.

## 14 août 2026

### Un fichier gzip abîmé répond 400

`POST /v1/partner/mint/batch` accepte un CSV compressé en gzip ou en zlib.

Un fichier gzip que le serveur ne parvient pas à décompresser répond
maintenant `400`, avec le motif dans le message. C'est une erreur de la
requête, et votre code peut la traiter comme telle.

Un fichier dont le contenu une fois décompressé dépasse le plafond répond
`413`, et le message indique ce plafond en Mio.

**Ce que vous faites.** Rien si vos envois sont valides. Si vous compressez à
la volée, vérifiez que le flux est complet avant de l'envoyer, et découpez vos
lots pour rester sous le plafond. Un lot porte au plus 500 articles.

### Le suivi de lot rend quatre champs de plus quand la file a oublié votre lot

`GET /v1/partner/mint/batch/status/{job_id}` répondait `unknown` au
propriétaire d'un lot ancien, une fois la durée de rétention de la file
écoulée. Il rend maintenant le statut réel, reconstruit à partir de la
réservation enregistrée à l'envoi du lot, et il ajoute quatre champs :
`batch_status`, `items_count`, `success_count` et `error_count`.

Un identifiant qui ne désigne aucun lot de votre marque répond toujours
`unknown`, sans rien révéler de plus.

**Ce que vous faites.** Rien, ces champs s'ajoutent.

## 13 août 2026

### Une intervention du portail partenaire porte un niveau de preuve

`POST /v1/partner-portal/interventions` enregistre désormais un niveau de
preuve avec chaque intervention, dans le champ `proof_level`.

Le corps de la requête accepte un champ facultatif `proof_code`. Sans lui, le
niveau vaut `declared`. Avec un code valide, il vaut `customer_code` quand le
code a été lu par le client, ou `work_order` quand il vient de l'ordre de
travail de la marque. Un code refusé répond `422` et n'enregistre rien du
tout : ni l'intervention, ni la consommation du code.

Les interventions enregistrées avant cette date rendent `proof_level` vide.
Une valeur vide ne vaut pas `declared` : la question n'avait pas été posée.

**Ce que vous faites.** Si vous affichez l'historique des interventions,
traitez la valeur vide comme un cas à part et n'affichez pas « déclaré » à sa
place.

## 11 août 2026

### Rupture de compatibilité. Supprimer un abonnement demande de retaper son adresse

`DELETE /v1/partner/webhooks/{webhook_id}` exige un paramètre d'URL `confirm`
qui contient l'adresse exacte de l'abonnement.

Sans ce paramètre, un abonnement qui vous appartient répond `400` avec le code
`CONFIRMATION_REQUIRED`. Avec une valeur qui ne correspond pas, la réponse est
`400` avec le code `CONFIRMATION_MISMATCH`, et l'API ne modifie rien. Une
suppression réussie répond `204`.

Un identifiant qui ne désigne aucun abonnement de votre marque répond `404`,
que vous ayez envoyé `confirm` ou non : l'API cherche votre abonnement avant
de regarder votre confirmation.

Un appel qui n'envoyait que l'identifiant est donc refusé.

**Ce que vous faites.** Lisez l'adresse avec `GET
/v1/partner/webhooks/{webhook_id}`, puis renvoyez-la dans `confirm`.

```bash
curl -X DELETE \
  "https://api.sealtrust.io/v1/partner/webhooks/1?confirm=https%3A%2F%2Fexemple-sas.example%2Fwebhooks%2Fsealtrust" \
  -H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000"
```

> [!DANGER] La suppression détruit le secret de signature
> Le point d'entrée cesse de recevoir des événements et son secret de
> signature est détruit. Un nouvel abonnement créé pour la même adresse reçoit
> un secret différent, et votre destinataire doit être reconfiguré avec ce
> nouveau secret.

### Chaque livraison porte le type d'événement

Chaque livraison porte maintenant `X-Webhook-Event`, qui nomme le type
d'événement. La signature ne couvre pas cet en-tête.

**Ce que vous faites.** Rien, cet en-tête s'ajoute.

> [!ATTENTION] L'en-tête `X-Webhook-Event` n'est pas couvert par la signature
> La signature couvre l'horodatage et le corps. Le type d'événement porté par
> `X-Webhook-Event` sert d'indication de routage et n'est pas authentifié. Si
> votre traitement dépend du type, déclarez une adresse par type d'événement.
> Le corps signé ne porte pas le type, donc `X-Webhook-Event` est la seule
> indication dont vous disposez, et elle n'est pas authentifiée.

## 14 juillet 2026

### Deux en-têtes s'ajoutent à chaque livraison

Chaque livraison porte maintenant `X-Webhook-Id` et `X-Webhook-Timestamp`, en
plus de `X-Webhook-Signature`.

`X-Webhook-Id` est l'empreinte du corps de la livraison. Les réessais d'une
même livraison portent la même valeur, ce qui vous permet d'écarter un
réessai. Deux événements distincts dont le corps est identique portent eux
aussi la même valeur.

**Ce que vous faites.** Dédoublonnez sur le couple formé du nom de
l'événement, porté par `X-Webhook-Event` depuis le 11 août 2026, et de
`X-Webhook-Id`. N'écartez jamais une livraison sur la valeur de
`X-Webhook-Timestamp` : cet horodatage change à chaque réessai, donc il vous
ferait traiter deux fois la même livraison.

### La signature porte les octets exacts qui vous sont envoyés

`X-Webhook-Signature` a la forme `t=<horodatage>,v1=<empreinte>`. L'empreinte
est un HMAC-SHA256 calculé avec le secret de votre abonnement, sur la chaîne
formée de l'horodatage, d'un point, et du corps de la requête.

Le corps signé est la suite d'octets reçue sur le fil. Décoder le JSON puis le
réencoder avant de vérifier casse la signature, parce que le réencodage ne
rend pas les mêmes octets.

**Ce que vous faites.** Vérifiez la signature sur le corps brut de la
requête, avant tout décodage.

## 12 juillet 2026

### Les points d'entrée d'abonnement exigent des droits

Lister vos abonnements et en lire un exigent le droit `webhooks:read`. Créer,
modifier et supprimer un abonnement exigent le droit `webhooks:write`.

Une clef qui ne porte pas le droit reçoit `403`, et le message nomme le droit
manquant.

**Ce que vous faites.** Vérifiez les droits de la clef qui gère vos
abonnements. Si elle a été créée avec le seul droit `mint:batch`, créez une
nouvelle clef portant aussi les droits de notification.

## 15 mars 2026

### Le préfixe `/v1` apparaît

Chaque point d'entrée métier devient joignable à deux adresses, avec le
préfixe `/v1` et sans aucun préfixe.

Les adresses sans préfixe répondent en plus deux en-têtes, `Deprecation: true`
et `Sunset: 2026-09-01`. Les adresses en `/v1` ne les portent pas. L'entrée du
16 août 2026 retire ces deux en-têtes et aucune date de fin de service n'a été
appliquée.

**Ce que vous faites.** Rien. Les deux adresses appellent le même code.

## Ce que cette page ne fait pas

Elle n'annonce pas les changements à venir. Une entrée arrive ici quand le
comportement décrit est en service.

Elle ne remplace pas un essai. Nous n'ouvrons aucun environnement de test aux
partenaires, et aucune adresse de bac à sable n'existe. Après avoir lu une
entrée marquée rupture, rejouez votre appel réel sur un produit ou un
abonnement de votre marque que vous pouvez sacrifier, et comparez le code de
réponse à celui que vous attendiez.
