# POST /partner/mint/batch

Envoyer un lot de lignes de produits, en JSON ou en CSV, jusqu'à 500 lignes par appel. Droit mint:batch. Les produits créés par cette voie sont identifiés par QR.

Source : https://docs.sealtrust.io/reference/post-partner-mint-batch/

---

Vous envoyez un lot de lignes de produits, et vous recevez un identifiant de
lot. Nous répondons dès que nous acceptons le lot, avant tout traitement. Vous
suivez ensuite l'avancement avec `GET /v1/partner/mint/batch/status/{job_id}`.
Lisez l'avertissement ci-dessous avant de lire ce suivi.

> [!DANGER] Ce point d'entrée crée des produits identifiés par QR, et rien d'autre
> Le serveur pose lui-même, sur chaque ligne, la méthode d'identification `qr`
> et l'identifiant technique du produit. Vous ne pouvez fournir ni l'un ni
> l'autre : une ligne qui les porte fait échouer la requête en 400. Un appel
> machine n'a aucune puce en main, donc cette voie ne produit aucun article
> NFC. Pour des articles NFC, passez par la console.

> [!ATTENTION] Le suivi dit où en est le travail, ses compteurs disent ce qui existe
> `GET /v1/partner/mint/batch/status/{job_id}` décrit d'abord le déroulement du
> traitement. Ce qui a été créé se lit dans ses compteurs, `success_count` et
> `error_count`, présents dès le premier appel. Un lot dont chaque ligne a été
> rejetée va quand même au bout de son traitement, et se lit alors
> `status: "failed"`, `is_finished: true` et `success_count: 0`. Un lot dont le
> traitement s'est arrêté en cours de route se lit lui aussi
> `status: "failed"`, avec cette fois un `success_count` non nul : ces articles
> existent, ne jetez pas le lot sans l'avoir lu.

Adresse complète :

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

Le même point d'entrée répond aussi sans le préfixe `/v1`, à
`https://api.sealtrust.io/partner/mint/batch`. Les deux adresses appellent le
même code. Utilisez la forme `/v1` pour une nouvelle intégration.

## Autorisation

Envoyez votre clef d'API dans l'en-tête `Authorization`, au format `Bearer`.
La clef doit porter le droit `mint:batch`. Une clef qui ne porte pas ce droit
reçoit un 403 dont le message nomme le droit manquant.

Trois conditions s'ajoutent au droit de la clef.

- L'offre de votre marque doit comprendre l'accès API. Sinon vous recevez un
  403 portant le code `FEATURE_NOT_AVAILABLE`.
- L'offre de votre marque doit autoriser l'identification par QR. C'est la
  méthode que le serveur pose sur chaque ligne de ce point d'entrée. Une offre
  qui ne l'autorise pas reçoit un 403 portant le code
  `AUTH_METHOD_NOT_ALLOWED`.
- Toutes les lignes du lot doivent porter le numéro de marque de la clef. Une
  seule ligne portant un autre numéro fait refuser le lot entier en 403.

## Plafond d'appels

Nous mesurons le débit sur une fenêtre fixe de 60 secondes. Nous appliquons
d'abord la valeur posée sur votre compte, si nous en avons posé une. Sinon
celle de votre offre. En l'absence des deux, la valeur de repli est de 120
appels par fenêtre.

Deux compteurs se superposent, avec le même plafond : un par clef, un pour la
somme de toutes les clefs de votre marque. Créer des clefs supplémentaires
n'augmente donc pas le débit total autorisé.

Dès que nous reconnaissons votre clef, la réponse porte quatre en-têtes. Ils
décrivent le compteur le plus contraignant des deux. Un refus
d'authentification arrive avant le comptage et ne porte aucun de ces en-têtes.

| En-tête | Contenu |
| --- | --- |
| `X-RateLimit-Limit` | le plafond appliqué sur la fenêtre |
| `X-RateLimit-Remaining` | ce qu'il vous reste dans la fenêtre en cours |
| `X-RateLimit-Reset` | l'horodatage de fin de la fenêtre, en secondes |
| `X-RateLimit-Scope` | `key` ou `brand`, le compteur qui a servi de référence |

Un refus de débit renvoie 429. Il porte ces quatre en-têtes, décrivant cette
fois le compteur qui a refusé, et il ajoute `Retry-After`, exprimé en secondes
restantes dans la fenêtre en cours. Cette valeur n'est jamais inférieure à 1.

Deux autres plafonds peuvent refuser le même appel : le quota quotidien de
votre clef, qui se consomme par article, et le quota mensuel de produits de
votre offre. Vous les retrouvez dans le tableau des erreurs, plus bas.

## Paramètres de chemin et de requête

Ce point d'entrée n'a ni paramètre de chemin ni paramètre de requête.

### En-têtes

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | oui | `Bearer` suivi de votre clef d'API. |
| `Content-Type` | `string` | oui | `application/json` pour un lot en JSON, `multipart/form-data` pour un fichier CSV. |
| `Idempotency-Key` | `string` | non | Votre propre identifiant d'appel. Nous le mémorisons 24 heures. Voir la section sur le rejeu. |

## Corps de la requête

Vous envoyez le lot dans l'un des deux formats. Le contenu est le même dans les
deux cas.

- **JSON** : envoyez une liste d'objets. Un objet seul, un nombre ou une chaîne
  au premier niveau fait échouer la requête en 400.
- **CSV** : envoyez un fichier en `multipart/form-data`, sous le champ de
  formulaire nommé exactement `file`. Encodez le fichier en UTF-8. Vous pouvez
  laisser la marque d'ordre d'octets en tête de fichier, nous la retirons. Un
  fichier encodé autrement fait échouer l'appel en 500. Dans un tableur,
  choisissez l'export « CSV UTF-8 ». Nous retirons les espaces de bord des
  cellules avant de valider.

Vous pouvez compresser le fichier CSV en gzip ou en zlib, nous le
décompressons. Si le contenu décompressé dépasse environ 20 Mio, vous recevez
un 413.

Un lot compte au maximum 500 articles. Nous refusons un lot vide en 400.

### Champs d'une ligne

Une ligne accepte cinq champs, et cinq seulement.

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `product_name` | `string` | oui | Le nom du produit. |
| `brand_id` | `integer` | oui | Le numéro de votre marque. Il doit être identique sur toutes les lignes et correspondre à la marque de la clef. |
| `category_id` | `integer` | oui | Le numéro de la catégorie du produit. La catégorie doit exister. |
| `metadata_uri` | `string` | oui | L'adresse des métadonnées du produit. |
| `external_ref` | `string` | non | Votre propre référence. Nous la conservons sur la ligne du lot. Aucun point d'entrée partenaire ne vous la rend : le suivi de lot ne la contient pas, et le produit créé ne la porte pas, aucun champ de produit ne la reprend. Vous la retrouvez uniquement dans le rapport d'erreur 400 d'un envoi CSV, qui nomme la ligne refusée. |

En CSV, l'en-tête doit contenir au minimum les quatre colonnes obligatoires.
L'ordre des colonnes est libre.

Le serveur n'ignore aucun champ. Tout champ que ce tableau ne nomme pas fait
échouer la ligne, et donc le lot, en 400.

> [!DANGER] `owner_email` et `contract_address` sont refusés depuis le 20/08/2026
> Ces deux champs étaient acceptés puis jetés sans être lus. Un partenaire qui
> frappait pour un client final croyait renseigner le propriétaire de
> l'article, et ne renseignait rien. Ils font aujourd'hui échouer la requête en
> 400. Retirez-les de votre code avant votre prochain appel.

### Rejouer un appel sans doubler son effet

Vous pouvez envoyer un en-tête `Idempotency-Key`. Il est facultatif. Nous le
mémorisons 24 heures.

- Rejouez la même clef d'idempotence avec le même lot, et vous recevez la
  réponse du premier appel, sans qu'un second traitement démarre.
- Rejouez la même clef d'idempotence avec un lot différent, et vous recevez un
  409. Ce comportement date du 20/08/2026. Auparavant, le second lot n'était
  jamais traité et rien ne le signalait.
- Rejouez une clef d'idempotence dont l'appel est encore en cours de
  traitement, et vous recevez aussi un 409. Ce verrou expire de lui-même au
  bout de 60 secondes.

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

> [!INFO] Ce qu'un rejeu consomme
> Un rejeu consomme un jeton de votre plafond de débit. Il ne consomme ni le
> quota mensuel de votre offre, ni le quota quotidien de votre clef.

## Requête d'exemple

Un lot de deux articles, en JSON.

:::onglets
```bash title="curl"
curl -i -X POST https://api.sealtrust.io/v1/partner/mint/batch \
  -H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: lot-exemple-0001" \
  -d '[
    {
      "product_name": "Sac Exemple 001",
      "brand_id": 12,
      "category_id": 3,
      "metadata_uri": "ipfs://exemple-metadonnees-0001",
      "external_ref": "EX-0001"
    },
    {
      "product_name": "Sac Exemple 002",
      "brand_id": 12,
      "category_id": 3,
      "metadata_uri": "ipfs://exemple-metadonnees-0002",
      "external_ref": "EX-0002"
    }
  ]'
```
```typescript
import { SealTrustClient } from "@sealtrust-io/sdk";

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

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",
    },
    {
      product_name: "Sac Exemple 002",
      brand_id: 12,
      category_id: 3,
      metadata_uri: "ipfs://exemple-metadonnees-0002",
      external_ref: "EX-0002",
    },
  ],
  "lot-exemple-0001",
);

console.log(lot.job_id, lot.status, lot.items_count, lot.brand_id);
```
```python
import requests

lot = [
    {
        "product_name": "Sac Exemple 001",
        "brand_id": 12,
        "category_id": 3,
        "metadata_uri": "ipfs://exemple-metadonnees-0001",
        "external_ref": "EX-0001",
    },
    {
        "product_name": "Sac Exemple 002",
        "brand_id": 12,
        "category_id": 3,
        "metadata_uri": "ipfs://exemple-metadonnees-0002",
        "external_ref": "EX-0002",
    },
]

response = requests.post(
    "https://api.sealtrust.io/v1/partner/mint/batch",
    headers={
        "Authorization": "Bearer st_test_0000000000000000000000000000000000000000000000",
        "Idempotency-Key": "lot-exemple-0001",
    },
    json=lot,
    timeout=30,
)

print(response.status_code)
print(response.headers["X-RateLimit-Remaining"])
print(response.json())
```
:::

> [!ATTENTION] Le SDK fabrique une clef d'idempotence si vous ne lui en donnez pas
> `products.mint()` envoie toujours un en-tête `Idempotency-Key`. Sans second
> argument, il en tire un nouveau à chaque appel, et deux appels successifs
> envoient donc deux lots. Passez votre propre valeur, comme dans l'exemple, si
> vous voulez qu'un nouvel essai reste sans effet.

Le même lot en CSV, envoyé en `multipart/form-data`. Le fichier `lot.csv`
contient :

```text title="lot.csv"
product_name,brand_id,category_id,metadata_uri,external_ref
Sac Exemple 001,12,3,ipfs://exemple-metadonnees-0001,EX-0001
Sac Exemple 002,12,3,ipfs://exemple-metadonnees-0002,EX-0002
```

Le SDK TypeScript n'expose pas l'envoi CSV. L'onglet TypeScript ci-dessous
appelle donc directement le point d'entrée.

:::onglets
```bash title="curl"
curl -i -X POST https://api.sealtrust.io/v1/partner/mint/batch \
  -H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000" \
  -H "Idempotency-Key: lot-exemple-0001" \
  -F "file=@lot.csv;type=text/csv"
```
```typescript
import { readFile } from "node:fs/promises";

const contenu = await readFile("lot.csv");

const formulaire = new FormData();
formulaire.append("file", new Blob([contenu], { type: "text/csv" }), "lot.csv");

const reponse = await fetch("https://api.sealtrust.io/v1/partner/mint/batch", {
  method: "POST",
  headers: {
    Authorization:
      "Bearer st_test_0000000000000000000000000000000000000000000000",
    "Idempotency-Key": "lot-exemple-0001",
  },
  body: formulaire,
});

console.log(reponse.status);
console.log(reponse.headers.get("X-RateLimit-Remaining"));
console.log(await reponse.json());
```
```python
import requests

with open("lot.csv", "rb") as fichier:
    response = requests.post(
        "https://api.sealtrust.io/v1/partner/mint/batch",
        headers={
            "Authorization": "Bearer st_test_0000000000000000000000000000000000000000000000",
            "Idempotency-Key": "lot-exemple-0001",
        },
        files={"file": ("lot.csv", fichier, "text/csv")},
        timeout=30,
    )

print(response.status_code)
print(response.headers["X-RateLimit-Remaining"])
print(response.json())
```
:::

## Réponse d'exemple

Code HTTP `200`.

```json
{
  "job_id": "0000a1b2c3d4e5f6",
  "status": "queued",
  "items_count": 2,
  "brand_id": 12
}
```

La réponse compte quatre champs et rien d'autre.

| Champ | Type | Description |
| --- | --- | --- |
| `job_id` | `string` | L'identifiant du lot. Passez-le à `GET /v1/partner/mint/batch/status/{job_id}`. |
| `status` | `string` | Vaut toujours `queued` sur cette réponse. |
| `items_count` | `integer` | Le nombre de lignes acceptées dans le lot. |
| `brand_id` | `integer` | Le numéro de votre marque. |

`status: "queued"` veut dire que nous avons accepté le lot et qu'il attend son
traitement. Nous n'avons encore rien traité à ce moment. Suivez la suite avec
`GET /v1/partner/mint/batch/status/{job_id}`.

## Erreurs

Le corps d'une réponse d'erreur porte un champ `detail`. Selon le cas, ce champ
contient une phrase ou un objet.

Un refus qui porte une phrase, ici le lot vide, code HTTP `400` :

```json
{
  "detail": "Batch vide (aucun item)"
}
```

Un refus qui porte un objet, ici un en-tête CSV incomplet, code HTTP `400` :

```json
{
  "detail": {
    "error": "Colonnes manquantes",
    "missing": ["category_id", "metadata_uri"]
  }
}
```

| Code | Condition | Que faire |
| --- | --- | --- |
| 400 | Le corps JSON envoyé est mal formé. | Vérifiez que vous envoyez du JSON valide, ou un fichier CSV en `multipart/form-data`. |
| 400 | Le corps JSON n'est pas une liste au premier niveau. | Enveloppez votre objet dans une liste, même pour un article seul. |
| 400 | Une ou plusieurs lignes JSON sont invalides. `detail` vaut `{"error": "JSON invalide", "rows": [...]}`, chaque entrée portant l'`index` de la ligne fautive et la raison. | Corrigez les lignes signalées. Le premier objet de la liste porte l'index 1. Aucune ligne du lot n'a été traitée. |
| 400 | Le fichier CSV est vide ou n'a pas de ligne d'en-tête. `detail` vaut `{"error": "CSV vide ou sans en-têtes"}`. | Ajoutez la ligne d'en-tête. |
| 400 | Une colonne obligatoire manque dans l'en-tête CSV. `detail` vaut `{"error": "Colonnes manquantes", "missing": [...]}`. | Ajoutez les colonnes nommées dans `missing`. Le fichier n'a pas été lu au-delà de son en-tête. |
| 400 | Une ou plusieurs lignes CSV sont invalides. `detail` vaut `{"error": "CSV invalide", "rows": [...]}`, chaque entrée portant la `line` du fichier, son `external_ref` et la raison. | Corrigez les lignes signalées. La première ligne de données porte le numéro 2. |
| 400 | Le fichier gzip envoyé est illisible. | Renvoyez le fichier, ou envoyez-le sans compression. |
| 400 | Le lot est vide. | Envoyez au moins une ligne. |
| 400 | Le lot dépasse 500 lignes. Le message donne le nombre reçu et le plafond. | Découpez votre envoi en plusieurs appels de 500 lignes au maximum. |
| 400 | Un ou plusieurs `category_id` ne correspondent à aucune catégorie. Le message liste les numéros introuvables. | Corrigez les numéros de catégorie. La liste des catégories est visible dans la console. |
| 401 | L'en-tête `Authorization` est absent. La réponse porte aussi `WWW-Authenticate: Bearer`. | Ajoutez l'en-tête. |
| 401 | L'en-tête `Authorization` ne commence pas par `Bearer ` suivi d'un espace. La réponse porte aussi `WWW-Authenticate: Bearer`. | Corrigez la forme de l'en-tête. |
| 401 | La valeur envoyée après `Bearer ` est vide ou fait moins de 40 caractères. | Envoyez le secret complet, sans espace ni retour à la ligne. |
| 401 | La clef envoyée est inconnue. | Vérifiez que vous utilisez une clef de cet environnement, et qu'elle n'a pas été recréée. |
| 403 | La clef n'est plus active, parce qu'elle a été révoquée. Le message donne son état. | 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 ne porte pas le droit `mint:batch`. Le message nomme le droit manquant. | Créez une clef portant ce droit. Les droits d'une clef existante ne se modifient pas. |
| 403 | L'offre de votre marque ne comprend pas l'accès API. `detail` vaut `{"code": "FEATURE_NOT_AVAILABLE", "feature": "api_access"}`. | Contactez-nous pour ouvrir ce droit sur votre offre. Réessayer ne changera rien. |
| 403 | L'offre de votre marque n'autorise pas l'identification par QR, qui est la méthode posée par ce point d'entrée. `detail` porte le code `AUTH_METHOD_NOT_ALLOWED`. | Contactez-nous pour ouvrir le QR sur votre offre. Réessayer ne changera rien. |
| 403 | Au moins une ligne porte un `brand_id` différent de celui de la clef. Le message donne le numéro attendu et le nombre de lignes fautives. | Corrigez les lignes. Aucune ligne du lot n'a été traitée. |
| 403 | Le lot ferait dépasser le quota mensuel de produits de votre offre. `detail` vaut `{"code": "QUOTA_EXCEEDED", "resource": "products", "current": ..., "additional": ..., "max": ..., "period": "monthly"}`. | Attendez la période suivante, réduisez la taille du lot, ou contactez-nous pour changer d'offre. Ce refus ne consomme pas le quota quotidien de votre clef. |
| 409 | La clef d'idempotence a déjà servi pour un lot différent dans les 24 dernières heures. | Utilisez une nouvelle clef d'idempotence pour ce lot. Ce lot n'a pas été traité. |
| 409 | Un appel portant la même clef d'idempotence est en cours de traitement. | Attendez la réponse du premier appel, puis relisez son résultat. Le verrou expire au bout de 60 secondes. |
| 413 | Le fichier compressé dépasse environ 20 Mio une fois décompressé. | Découpez votre fichier. Un lot de 500 lignes reste très en dessous de ce plafond. |
| 429 | Le plafond de débit de la clef est atteint. `Retry-After` et la famille `X-RateLimit-*` accompagnent la réponse, avec `X-RateLimit-Scope: key`. | Attendez le nombre de secondes indiqué par `Retry-After`, puis réessayez. |
| 429 | Le plafond de débit de la marque est atteint, toutes clefs confondues. `X-RateLimit-Scope` vaut `brand`. | Attendez le nombre de secondes indiqué par `Retry-After`. Créer une clef supplémentaire ne relève pas ce plafond. |
| 429 | Le quota quotidien de la clef est atteint. Les en-têtes `X-Quota-Limit`, `X-Quota-Remaining` et `X-Quota-Reset` accompagnent la réponse. | Attendez le passage de minuit en temps universel, ou faites relever le quota de la clef. Le quota se consomme par article : un lot de 100 lignes en consomme 100. |
| 500 | Le fichier CSV envoyé n'est pas encodé en UTF-8. | Réenregistrez le fichier en UTF-8. Dans un tableur, choisissez l'export « CSV UTF-8 ». |
| 500 | Votre liste JSON contient un élément qui n'est pas un objet, par exemple une chaîne ou un nombre. | Envoyez une liste dont chaque élément est un objet portant les champs décrits plus haut. |
| 500 | Un `category_id` hors des bornes que la base accepte. | Envoyez des numéros de catégorie visibles dans la console. |
| 500 | La marque rattachée à votre clef est introuvable. | Contactez-nous en indiquant l'heure de l'appel. |
| 500 | Une erreur inattendue s'est produite pendant le traitement de votre appel. | Réessayez. Si l'erreur persiste, contactez le support en indiquant l'heure de l'appel. |
| 503 | Le service de plafonnement des appels est momentanément indisponible. Nous refusons alors l'appel. | Réessayez dans quelques instants. Aucune ligne n'a été traitée. |

> [!ATTENTION] L'ordre des contrôles a des conséquences sur vos quotas
> Nous vérifions le quota mensuel de votre offre avant de consommer le quota
> quotidien de votre clef. Un lot refusé par votre offre ne brûle donc pas
> votre quota du jour. À l'inverse, un lot accepté consomme le quota quotidien
> à hauteur du nombre de lignes, au moment de l'acceptation, avant tout
> traitement. Nous consommons ce quota avant la mise en file. Un appel qui
> échoue après cette étape rend un 500 et laisse le quota consommé. Comptez-le
> dans vos nouvelles tentatives.

## Voir aussi

- [`GET /partner/mint/batch/status/{job_id}`](/reference/get-partner-mint-batch-status/),
  suivre l'avancement d'un lot envoyé à la frappe.
- [Créer des produits, à l'unité et en lot](/creer-des-produits/),
  créer un produit, créer une série entière, importer un fichier.
- [API partenaire, vue d'ensemble](/api-vue-ensemble/),
  adresse de base, clefs, droits, plafonds d'appels et pagination.
- [Erreurs de l'API](/api-erreurs/),
  reconnaître un code d'erreur et décider s'il faut corriger ou rejouer.
