# API partenaire, vue d'ensemble

Adresse de base, authentification par clef, droits, plafonds d'appels, rejeu d'un appel sans doubler son effet, pagination et versionnage. La page à lire avant d'ouvrir la référence.

Source : https://docs.sealtrust.io/api-vue-ensemble/

---

En quittant cette page, vous saurez authentifier un appel à l'API partenaire,
choisir la bonne adresse, lire les en-têtes qui vous disent combien d'appels il
vous reste, rejouer une requête sans doubler son effet, et reconnaître les trois
plafonds qui peuvent vous refuser. C'est la page à lire une fois, avant
d'ouvrir la référence point d'entrée par point d'entrée.

## Ce que cette API fait

L'API partenaire à clef sert à piloter SealTrust depuis votre propre système,
sans passer par la console. Elle compte huit points d'entrée, répartis en trois
usages.

| Usage | Points d'entrée |
| --- | --- |
| Créer des produits en lot, et suivre l'avancement du traitement | 2 |
| Déclarer une vente au client final | 1 |
| Gérer vos abonnements aux notifications | 5 |

L'envoi d'un lot ne crée que des produits identifiés par QR. Le serveur pose
lui-même, sur chaque ligne du lot, la méthode d'identification et l'identifiant
technique de l'article. Vos lignes ne portent ni l'un ni l'autre, et vous ne
pouvez pas en demander d'autres.

> [!ATTENTION] `finished` décrit la file d'exécution, jamais le résultat
> `POST /partner/mint/batch` répond `200` dès que nous acceptons le lot, avant
> tout traitement. Le suivi répond ensuite `status: "finished"` dès que la file
> d'exécution a fini de traiter le lot, quel que soit le sort de chaque ligne.
> Un lot dont toutes les lignes ont été rejetées répond `finished` lui aussi.
> Ne branchez donc pas votre détection d'échec sur `status: "failed"`. Quand la
> file d'exécution ne garde plus le lot, la réponse est construite depuis votre
> réservation et porte quatre champs de plus : `batch_status`, `items_count`,
> `success_count` et `error_count`. Un lot entièrement rejeté se règle alors
> avec `status: "failed"`, `success_count: 0` et un `error_count` égal au
> nombre de lignes envoyées.

Il n'y a pas de point d'entrée pour lister vos produits ni pour en lire un seul
avec une clef d'API. Vous lisez un produit par les points d'entrée publics de
vérification, qui ne demandent aucune clef.

Le portail partenaire, à l'adresse `/partner-portal`, est une surface
différente. Il s'authentifie avec la session d'un compte partenaire réparateur
ou recycleur, et une clef d'API n'y donne aucun accès.

## L'adresse de base et le préfixe /v1

Vous appelez l'API sur `https://api.sealtrust.io`.

Chaque point d'entrée décrit sur ce site existe à deux adresses qui appellent
exactement le même code : avec le préfixe `/v1`, et sans aucun préfixe.

```http
POST https://api.sealtrust.io/v1/partner/mint/batch
POST https://api.sealtrust.io/partner/mint/batch
```

Utilisez la forme `/v1` pour toute nouvelle intégration.

Les pages de référence de ce site titrent chaque point d'entrée avec son chemin
sans préfixe, par exemple `POST /partner/mint/batch`. Ajoutez `/v1` devant ce
chemin quand vous écrivez votre appel.

Les adresses sans préfixe sont des alias permanents. Nous ne leur attachons
aucune date de fin de service, et nous n'envoyons sur ces réponses aucun
en-tête d'annonce de suppression. Si votre intégration les appelle déjà, elle continuera
de fonctionner.

> [!INFO] Ce que sera une éventuelle version 2
> Le jour où une évolution cassante arrivera, elle s'ajoutera sous `/v2`. La
> racine restera figée sur le comportement de la version 1. Une intégration
> installée ne se met pas à jour toute seule, et notre engagement porte
> là-dessus.

## Authentifier un appel

Vous envoyez votre clef dans l'en-tête `Authorization`, au format `Bearer`.

```http
Authorization: Bearer votre_clef
```

C'est le seul mode d'authentification de cette API. Il n'y a ni paramètre
d'URL, ni cookie, ni signature de requête à calculer.

### Obtenir une clef

Vous créez vos clefs depuis la console de votre marque, dans **Paramètres**
puis l'onglet **Développeurs**. L'onglet reste visible quelle que soit votre
offre. Si votre offre ne comprend pas l'accès API, l'écran s'affiche verrouillé
et vous ne pouvez y créer aucune clef.

Trois points comptent au moment de la création.

- **Le secret complet ne s'affiche qu'une seule fois**, dans la fenêtre qui
  suit la création. Copiez-le à ce moment. Aucun écran et aucun point d'entrée
  ne permet de le relire ensuite. Nous n'en conservons qu'une empreinte.
- **Une clef appartient à une seule marque.** Nous rattachons à cette marque
  toutes les opérations faites avec elle, et à aucune autre.
- **Une clef expire toujours.** Vous choisissez une durée de vie à la création.
  Si vous n'en choisissez pas, elle est de 365 jours. Notez la date dans votre
  agenda : le jour venu, vos appels s'arrêtent.

La console affiche ensuite les premiers caractères de chaque clef, pour vous
permettre de la reconnaître dans la liste sans jamais réafficher son secret.
Elle affiche aussi la date de dernière utilisation. Nous ne rafraîchissons
cette date qu'au plus une fois par minute, elle peut donc retarder d'une minute
sur votre dernier appel.

### Révoquer une clef

Vous révoquez une clef depuis la même page, et la révocation prend effet
immédiatement. Le premier appel qui suit reçoit un refus. Révoquer une clef libère une place si
votre offre limite le nombre de clefs actives.

> [!DANGER] Une clef compromise se révoque
> Il n'existe aucun moyen de faire tourner le secret d'une clef existante. Si
> un secret a pu fuiter, révoquez la clef et créez-en une nouvelle, puis
> remplacez la valeur dans votre système.

### Les refus d'authentification

| Code | Ce qui s'est passé | Que faire |
| --- | --- | --- |
| 401 | l'en-tête `Authorization` est absent | ajoutez-le, la réponse porte aussi `WWW-Authenticate: Bearer` |
| 401 | l'en-tête ne commence pas par `Bearer ` suivi d'un espace | corrigez la forme de l'en-tête |
| 401 | la clef est mal formée ou inconnue | renvoyez le secret complet tel que la console l'a affiché, sans espace ni retour à la ligne |
| 403 | la clef a été révoquée | créez une nouvelle clef dans la console |
| 403 | la clef a atteint sa date d'expiration | créez une nouvelle clef, l'ancienne ne redeviendra jamais valide |
| 403 | la clef n'a pas le droit requis par ce point d'entrée | le message nomme le droit manquant, voir [les droits attachés à une clef](#les-droits-attachés-à-une-clef) |

## Votre premier appel

Vérifiez qu'une clef fonctionne avec la liste de vos abonnements aux
notifications. Ce point d'entrée ne modifie rien et ne consomme aucun quota. Il demande le droit `webhooks:read`.

:::onglets
```bash title="curl"
curl -i https://api.sealtrust.io/v1/partner/webhooks \
  -H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000"
```
```typescript
import { SealTrustClient } from "@sealtrust-io/sdk";

const client = new SealTrustClient({
  apiKey: "st_test_0000000000000000000000000000000000000000000000",
});

const page = await client.webhooks.list();

console.log(page.total);
console.log(page.items);
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/partner/webhooks",
    headers={
        "Authorization": "Bearer st_test_0000000000000000000000000000000000000000000000"
    },
    timeout=30,
)

print(response.status_code)
print(response.json())
```
:::

Réponse, code HTTP 200, pour une marque qui n'a encore aucun abonnement :

```json
{
  "items": [],
  "total": 0
}
```

Le SDK TypeScript vous rend le corps de la réponse, jamais ses en-têtes. Pour
lire les en-têtes de plafond décrits dans
[Le débit, en appels par minute](#le-débit-en-appels-par-minute), appelez l'API en HTTP direct
depuis votre serveur, ou passez par `curl -i`. Un navigateur ne peut pas les
lire : nous n'exposons au navigateur que l'en-tête `X-Total-Count`.

## Les droits attachés à une clef

Chaque clef porte une liste de droits. Un droit absent fait refuser l'appel en
403, et le message de refus nomme le droit qui manque.

Quatre droits commandent l'accès aux points d'entrée de cette API.

| Droit | Ce qu'il ouvre |
| --- | --- |
| `mint:batch` | envoyer un lot de produits, et lire l'avancement d'un lot |
| `sellout:write` | déclarer une vente au client final |
| `webhooks:read` | lister vos abonnements, et en lire un seul |
| `webhooks:write` | créer, modifier et supprimer un abonnement |

> [!ATTENTION] Ne cochez que ce dont vous avez besoin
> Le formulaire de création propose d'autres cases. Elles n'ouvrent aujourd'hui
> aucun des huit points d'entrée décrits sur ce site. Cochez les droits du
> tableau ci-dessus, et uniquement ceux que votre intégration utilise
> réellement.

## Les trois plafonds

Trois compteurs différents peuvent refuser un appel. Ils ne se ressemblent pas
et ils ne se lisent pas au même endroit. Traitez-les séparément.

### Le débit, en appels par minute

Nous comptons vos appels sur une fenêtre fixe de 60 secondes. Ce plafond
s'applique aux huit points d'entrée, y compris la lecture du statut d'un lot et
la suppression d'un abonnement.

Deux compteurs tournent en même temps : un pour votre clef, un pour la somme de
toutes les clefs de votre marque. Le plafond est le même des deux côtés.

> [!INFO] Ajouter des clefs n'achète pas de débit
> Le compteur de marque borne le total. Créer une deuxième clef pour doubler
> votre cadence ne fonctionne pas : les deux clefs alimentent le même compteur
> de marque.

Votre plafond vient d'une valeur posée sur votre compte, ou à défaut de votre
offre. Quand aucune des deux n'est définie, vous disposez de 120 appels par
fenêtre. Ne devinez pas cette valeur : vous la lisez sur chaque réponse.

Toute réponse qui passe le plafond porte les quatre en-têtes suivants.

| En-tête | Ce qu'il contient |
| --- | --- |
| `X-RateLimit-Limit` | le plafond applicable, en appels par fenêtre |
| `X-RateLimit-Remaining` | ce qu'il vous reste dans la fenêtre en cours |
| `X-RateLimit-Reset` | l'horodatage, en secondes depuis 1970, où la fenêtre repart |
| `X-RateLimit-Scope` | `key` ou `brand`, selon celui des deux compteurs qui est le plus contraignant |

> [!ATTENTION] Trois de ces en-têtes arrivent en deux exemplaires
> Un second compteur, celui de l'adresse d'où part l'appel, s'applique à toute
> notre API et pose lui aussi `X-RateLimit-Limit`, `X-RateLimit-Remaining` et
> `X-RateLimit-Reset`. La réponse porte donc deux valeurs pour chacun de ces
> trois en-têtes, et la plupart des clients HTTP vous les rendent collées et
> séparées par une virgule. Ne lisez aucun des trois comme un nombre.
> `X-RateLimit-Scope` n'apparaît qu'une fois, et c'est le seul en-tête qui
> désigne à coup sûr le compteur de votre clef ou de votre marque.

Ces en-têtes décrivent toujours le compteur le plus serré des deux. Pilotez
votre cadence dessus, et ralentissez avant d'atteindre zéro.

Un dépassement renvoie 429 avec les mêmes en-têtes, plus `Retry-After`.
`Retry-After` compte les secondes qui restent dans la fenêtre en cours, et vaut
au minimum 1. Respectez-le. `X-RateLimit-Scope` vous dit lequel des deux
compteurs a refusé, ce qui évite de chercher du côté de la clef quand c'est le
total de la marque qui est plein.

Un dernier cas, rare : si le service qui tient ces compteurs est
momentanément indisponible, les huit points d'entrée répondent 503. Un 503
signifie que l'appel n'a rien fait du tout. Réessayez plus tard.

### Le quota quotidien de la clef

Chaque clef peut porter un quota quotidien. Une clef sans quota est illimitée
de ce côté. Le compteur repart de zéro au passage de minuit en temps universel.
Votre fuseau horaire n'entre pas en compte.

Ce quota se compte par ligne envoyée.

- Un lot de 100 lignes consomme 100 unités.
- Une déclaration de vente consomme 1 unité.
- Les six autres points d'entrée ne consomment rien : les cinq points d'entrée
  d'abonnement aux notifications, et la lecture de l'avancement d'un lot.

Un dépassement renvoie 429 avec trois en-têtes qui lui sont propres.

| En-tête | Ce qu'il contient |
| --- | --- |
| `X-Quota-Limit` | le quota quotidien de la clef |
| `X-Quota-Remaining` | ce qu'il reste pour aujourd'hui |
| `X-Quota-Reset` | la date de la dernière remise à zéro du compteur |

La réponse porte en plus les quatre en-têtes `X-RateLimit-*` du contrôle de
débit, que l'appel venait de passer avant d'être arrêté par le quota.

> [!ATTENTION] Deux plafonds différents rendent 429
> Un 429 ne dit pas à lui seul lequel des deux compteurs a parlé. Regardez deux
> en-têtes, et deux seulement. `Retry-After` n'apparaît que sur un refus de
> débit, qui se rattrape en quelques secondes. `X-Quota-Limit` n'apparaît que
> sur un refus de quota quotidien, qui ne se rattrape qu'au prochain minuit
> universel. Les en-têtes `X-RateLimit-*` accompagnent les deux refus, ils ne
> distinguent rien.

### Le quota mensuel de votre offre

Votre offre fixe un nombre de produits créables par mois, sur votre période de
facturation. Elle décide aussi si l'accès API vous est ouvert, et si les
abonnements aux notifications vous sont ouverts.

Trois refus en 403 en découlent, tous porteurs d'un code lisible par votre
programme.

Votre offre ne comprend pas l'accès API :

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

Votre offre ne comprend pas les abonnements aux notifications :

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

Ce refus-là ne touche que la création et la modification d'un abonnement. Vous
gardez la lecture et la suppression quelle que soit votre offre. Vous gardez
aussi l'extinction d'un abonnement, à une condition : envoyez `is_active` à
faux et rien d'autre. Dès qu'un second champ accompagne cette valeur, l'appel
compte comme une modification et le refus s'applique.

Vous avez atteint le nombre de produits de votre période de facturation :

```json
{
  "detail": {
    "code": "QUOTA_EXCEEDED",
    "resource": "products",
    "current": 4800,
    "additional": 300,
    "max": 5000,
    "period": "monthly"
  }
}
```

Celui-ci se lit ainsi : vous avez déjà créé 4800 produits sur la période, vous
en demandez 300 de plus, le maximum est de 5000. Découpez votre lot ou attendez
la période suivante.

## Rejouer un appel sans doubler son effet

L'envoi d'un lot accepte un en-tête `Idempotency-Key`. Cet en-tête porte une
clef d'idempotence, c'est-à-dire une valeur qui garantit qu'un même envoi
renvoyé deux fois ne produit qu'un seul traitement. Vous en choisissez la
valeur, et vous la gardez le temps de vos tentatives.

```http
Idempotency-Key: lot-exemple-0001
```

Cet en-tête répond au cas où vous n'obtenez pas de réponse : coupure réseau,
délai dépassé, redémarrage de votre côté. Vous ne savez pas si le lot est
parti. Renvoyez la même requête avec la même valeur, et vous obtenez la réponse
du premier appel sans qu'un second lot parte en traitement.

Trois comportements à connaître.

- **Même clef d'idempotence, même lot** : vous récupérez la réponse du premier
  appel. Nous ne lançons pas un second traitement.
- **Même clef d'idempotence, lot différent** : la réponse est 409. Nous ne vous
  rendons pas la réponse du premier appel, parce qu'elle ne décrit pas ce que
  vous venez d'envoyer.
- **Même clef d'idempotence pendant que le premier appel est encore en cours de
  traitement** : la réponse est également 409. Attendez la fin du premier
  appel, puis réessayez.

Nous gardons une valeur 24 heures. Passé ce délai, la même valeur redevient une
requête neuve, et un renvoi enverrait un second lot en traitement. N'utilisez
donc jamais une valeur fixe : tirez une valeur nouvelle par lot, et gardez-la le
temps des tentatives de ce lot.

La comparaison entre deux lots porte sur leur contenu une fois normalisé.
L'ordre des lignes, l'ordre des colonnes, le format choisi entre CSV et JSON et
les cellules laissées vides ne changent rien : nous reconnaissons deux envois du
même contenu comme identiques.

Un renvoi qui rend la réponse mise en cache consomme quand même un appel sur
votre plafond de débit. Il ne consomme ni votre quota quotidien, ni le quota
mensuel de votre offre.

> [!ATTENTION] L'idempotence ne couvre qu'un seul point d'entrée
> Seul l'envoi d'un lot lit cet en-tête. La déclaration de vente au client
> final ne le lit pas. Elle ne crée pas de doublon pour autant : elle vous
> répond `already_activated` si le produit avait déjà été déclaré. Chaque
> renvoi consomme malgré tout une unité de votre quota quotidien.

## Pagination

Un seul point d'entrée de cette API rend une liste : `GET /v1/partner/webhooks`.

| Paramètre | Type | Valeur par défaut | Description |
| --- | --- | --- | --- |
| `skip` | `entier` | `0` | nombre d'éléments à sauter, à partir de 0 |
| `limit` | `entier` | `20` | nombre d'éléments à rendre, entre 1 et 100 |

La réponse contient deux champs : `items`, la page demandée, et `total`, le
nombre total d'abonnements de votre marque. Vous parcourez donc la liste en
augmentant `skip` de la valeur de `limit` jusqu'à couvrir `total`.

> [!INFO] La réponse ne vous renvoie pas votre pagination
> La réponse ne reprend ni `skip` ni `limit`. Gardez-les de votre côté pendant
> le parcours.

Vous recevez les abonnements du plus récent au plus ancien.

## Ce qu'il faut retenir avant d'ouvrir la référence

- L'adresse est `https://api.sealtrust.io`, et la forme à utiliser est `/v1`.
- La clef part dans `Authorization: Bearer`, et nulle part ailleurs.
- Le secret ne s'affiche qu'une fois. Une clef expire toujours.
- Un 401 parle de la clef elle-même. Un 403 parle d'un droit, d'un statut, ou
  de ce que votre offre autorise.
- Un 429 vient soit du débit, soit du quota quotidien. `Retry-After` désigne le
  débit, `X-Quota-Limit` désigne le quota quotidien.
- Un 503 vient du service qui tient les compteurs de débit, et signifie que
  l'appel n'a rien fait.
- Le suivi d'un lot répond `finished` quand la file d'exécution a fini, jamais
  pour dire que les lignes ont réussi.
- Une requête de lot renvoyée après une coupure doit porter la même
  `Idempotency-Key` que la première tentative.

Chaque point d'entrée a sa propre page, avec ses paramètres, sa réponse réelle
et son tableau d'erreurs complet. Vous les retrouvez dans la barre latérale,
rangés par tâche : « Créer des produits », « Déclarer une vente », « Recevoir
les événements ».

- [POST /partner/mint/batch](/reference/post-partner-mint-batch/)
- [GET /partner/mint/batch/status/{job_id}](/reference/get-partner-mint-batch-status/)
- [POST /partner/sellout](/reference/post-partner-sellout/)
- [POST /partner/webhooks](/reference/post-partner-webhooks/)
- [GET /partner/webhooks](/reference/get-partner-webhooks/)
- [GET /partner/webhooks/{webhook_id}](/reference/get-partner-webhooks-id/)
- [PUT /partner/webhooks/{webhook_id}](/reference/put-partner-webhooks-id/)
- [DELETE /partner/webhooks/{webhook_id}](/reference/delete-partner-webhooks-id/)

La page [Erreurs](/api-erreurs/) rassemble les codes communs à toute l'API.
