Method POST/partner /mint /batch
Send a batch of product rows, in JSON or in CSV, up to 500 rows per call. Scope mint:batch. The products created by this path are identified by QR.
On this page
You send a batch of product rows, and you receive a batch
identifier. We answer as soon as we accept the batch, before any processing. You
then follow the progress with GET /v1/partner/mint/batch/status/{job_id}.
Read the warning below before reading that status.
Full address:
POST https://api.sealtrust.io/v1/partner/mint/batchThe same endpoint also answers without the /v1 prefix, at
https://api.sealtrust.io/partner/mint/batch. The two addresses call the
same code. Use the /v1 form for a new integration.
#
Send your API key in the Authorization header, in the Bearer format.
The key must carry the mint:batch scope. A key that does not carry this scope
receives a 403 whose message names the missing scope.
Three conditions come on top of the scope of the key.
- The plan of the key's brand must include API access. Otherwise you receive
a 403 carrying the code
FEATURE_NOT_AVAILABLE. - The plan of the batch's brand must allow identification by QR. That is the
method the server sets on every row of this endpoint. A plan
that does not allow it receives a 403 carrying the code
AUTH_METHOD_NOT_ALLOWED. - Every row carries the public code of a brand the key serves: its own, or,
with a reseller key, a client brand of your contract. A single row carrying
another code makes the whole batch refused with a 403. All the rows carry the same
brand, otherwise the batch is refused with 422
ONE_BRAND_PER_BATCH. The offer and the allowance applied are that brand's.
#Rate limit
We measure the rate over a fixed 60-second window. We first apply the value set on your account, if we have set one. Otherwise the one of your plan. In the absence of both, the fallback value is 120 calls per window.
Two counters are layered, with the same cap: one per key, one for the sum of all the keys of your brand. Creating additional keys therefore does not increase the total rate allowed.
As soon as we recognize your key, the response carries four headers. They describe the more constraining of the two counters. An authentication refusal comes before the counting and carries none of these headers.
| Header | Content |
|---|---|
X-RateLimit-Limit | the cap applied over the window |
X-RateLimit-Remaining | what is left to you in the current window |
X-RateLimit-Reset | the end-of-window timestamp, in seconds |
X-RateLimit-Scope | key or brand, the counter used as the reference |
A rate refusal returns 429. It carries these four headers, describing this
time the counter that refused, and it adds Retry-After, expressed in seconds
remaining in the current window. This value is never lower than 1.
Two other caps can refuse the same call: the daily quota of your key, which is consumed per item, and, on a trial account only, the cap of 10 products per billing period. You will find them in the error table, further down.
#Path and query parameters
This endpoint has no path parameter, and one optional query parameter.
| Name | Type | Required | Description |
|---|---|---|---|
issue_purchase_codes | boolean | no | true asks for a purchase code for each piece of the batch. The key must also carry the claim_codes:issue scope, otherwise 403 before the batch is read. The codes come once with GET /v1/partner/mint/batch/{job_id}/items. See Issue purchase codes. |
#Headers
| Name | Type | Required | Description |
|---|---|---|---|
Authorization | string | yes | Bearer followed by your API key. |
Content-Type | string | yes | application/json for a batch in JSON, multipart/form-data for a CSV file. |
Idempotency-Key | string | no | Your own call identifier. We remember it for 24 hours. See the section on replaying. |
#Request body
You send the batch in one of the two formats. The content is the same in both cases.
- JSON: send a list of objects. A lone object, a number or a string at the top level makes the request fail with a 400.
- CSV: send a file as
multipart/form-data, under the form field named exactlyfile. Encode the file in UTF-8. You may leave the byte order mark at the head of the file, we remove it. A file encoded otherwise makes the call fail with a 500. In a spreadsheet, choose the "CSV UTF-8" export. We strip the edge spaces of the cells before validating.
You can compress the CSV file in gzip or in zlib, we decompress it. If the decompressed content exceeds about 20 MiB, you receive a 413.
A batch has at most 500 items. We refuse an empty batch with a 400.
#Fields of a row
A row accepts seven fields, and only seven.
| Name | Type | Required | Description |
|---|---|---|---|
product_name | string | yes | The name of the product. |
brand_code | string | yes | The public code of the batch's brand, ten characters, for example 7K3QXW9M2A. It must name the same brand on every row. It is the key's brand, or, with a reseller key, one of your client brands. GET /v1/partner/catalog/brands gives these codes. The old brand_id is still accepted in its place for now, but it is deprecated, and is written with digits only (12 or "12"). A row that carries both, or neither, is refused with 400. A badly written code or number is refused with 422 in a JSON body, with 400 in a CSV, see the errors below. |
category_id | integer | yes | The number of the category of the product. The category must exist. |
metadata_uri | string | yes | The address of the metadata of the product. |
model_sku | string | no | The SKU of a model of your catalogue, as you sent it through PUT /v1/partner/catalog/brands/{brand_code}/models. The piece is attached to that model: it gets its GS1 link if the model has a GTIN, and its page shows the model passport. Without this field, or batch_code, the piece has neither model nor batch. |
batch_code | string | no | The code of a batch of your catalogue (PUT .../batches). The piece is attached to that batch and to its model, and its page shows the batch passport. A batch already anchored on the blockchain accepts no more pieces. |
external_ref | string | no | Your own reference. We keep it on the row of the batch, and GET /partner/mint/batch/{job_id}/items returns it next to the serial of the piece created. The product created does not carry it. |
In CSV, the header must contain at least the four required columns. The model_sku and batch_code columns are optional, and an empty cell means "no code".
The order of the columns is free.
The server ignores no field. Any field this table does not name makes the row fail, and therefore the batch, with a 400.
#Replaying a call without doubling its effect
You can send an Idempotency-Key header. It is optional. We remember it
for 24 hours.
- Replay the same idempotency key with the same batch, and you receive the response of the first call, without a second processing starting.
- Replay the same idempotency key with a different batch, and you receive a
- This behavior dates from 08/20/2026. Before that, the second batch was never processed and nothing reported it.
- Replay an idempotency key whose call is still being processed, and you also receive a 409. This lock expires on its own after 60 seconds.
We compare two batches on their content once read and normalized. The order of the rows, the order of the columns, the choice between CSV and JSON and the empty cells make no difference.
#Example request
A batch of two items, in JSON.
curl -i -X POST https://api.sealtrust.io/v1/partner/mint/batch \
-H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: batch-example-0001" \
-d '[
{
"product_name": "Sac Exemple 001",
"brand_code": "7K3QXW9M2A",
"category_id": 3,
"metadata_uri": "ipfs://example-metadonnees-0001",
"external_ref": "EX-0001"
},
{
"product_name": "Sac Exemple 002",
"brand_code": "7K3QXW9M2A",
"category_id": 3,
"metadata_uri": "ipfs://example-metadonnees-0002",
"external_ref": "EX-0002"
}
]'import { SealTrustClient } from "@sealtrust-io/sdk";
const sealtrust = new SealTrustClient({
apiKey: "st_live_0000000000000000000000000000000000000000000000",
});
const batch = await sealtrust.products.mint(
[
{
product_name: "Sac Exemple 001",
brand_code: "7K3QXW9M2A",
category_id: 3,
metadata_uri: "ipfs://example-metadonnees-0001",
external_ref: "EX-0001",
},
{
product_name: "Sac Exemple 002",
brand_code: "7K3QXW9M2A",
category_id: 3,
metadata_uri: "ipfs://example-metadonnees-0002",
external_ref: "EX-0002",
},
],
"batch-example-0001",
);
console.log(batch.job_id, batch.status, batch.items_count, batch.brand_code);import requests
batch = [
{
"product_name": "Sac Exemple 001",
"brand_code": "7K3QXW9M2A",
"category_id": 3,
"metadata_uri": "ipfs://example-metadonnees-0001",
"external_ref": "EX-0001",
},
{
"product_name": "Sac Exemple 002",
"brand_code": "7K3QXW9M2A",
"category_id": 3,
"metadata_uri": "ipfs://example-metadonnees-0002",
"external_ref": "EX-0002",
},
]
response = requests.post(
"https://api.sealtrust.io/v1/partner/mint/batch",
headers={
"Authorization": "Bearer st_live_0000000000000000000000000000000000000000000000",
"Idempotency-Key": "batch-example-0001",
},
json=batch,
timeout=30,
)
print(response.status_code)
print(response.headers["X-RateLimit-Remaining"])
print(response.json())The same batch in CSV, sent as multipart/form-data. The batch.csv file
contains:
product_name,brand_code,category_id,metadata_uri,external_ref
Sac Exemple 001,7K3QXW9M2A,3,ipfs://example-metadonnees-0001,EX-0001
Sac Exemple 002,7K3QXW9M2A,3,ipfs://example-metadonnees-0002,EX-0002The TypeScript SDK does not expose CSV submission. The TypeScript tab below therefore calls the endpoint directly.
curl -i -X POST https://api.sealtrust.io/v1/partner/mint/batch \
-H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
-H "Idempotency-Key: batch-example-0001" \
-F "file=@batch.csv;type=text/csv"import { readFile } from "node:fs/promises";
const contenu = await readFile("batch.csv");
const formulaire = new FormData();
formulaire.append("file", new Blob([contenu], { type: "text/csv" }), "batch.csv");
const response = await fetch("https://api.sealtrust.io/v1/partner/mint/batch", {
method: "POST",
headers: {
Authorization:
"Bearer st_live_0000000000000000000000000000000000000000000000",
"Idempotency-Key": "batch-example-0001",
},
body: formulaire,
});
console.log(response.status);
console.log(response.headers.get("X-RateLimit-Remaining"));
console.log(await response.json());import requests
with open("batch.csv", "rb") as file:
response = requests.post(
"https://api.sealtrust.io/v1/partner/mint/batch",
headers={
"Authorization": "Bearer st_live_0000000000000000000000000000000000000000000000",
"Idempotency-Key": "batch-example-0001",
},
files={"file": ("batch.csv", file, "text/csv")},
timeout=30,
)
print(response.status_code)
print(response.headers["X-RateLimit-Remaining"])
print(response.json())#Example response
HTTP code 200.
{
"job_id": "0000a1b2c3d4e5f6",
"status": "queued",
"items_count": 2,
"brand_code": "7K3QXW9M2A",
"brand_id": 12,
"issue_purchase_codes": false
}The response has six fields and nothing else.
| Field | Type | Description |
|---|---|---|
job_id | string | The identifier of the batch. Pass it to GET /v1/partner/mint/batch/status/{job_id}. |
status | string | Always queued on this response. |
items_count | integer | The number of rows accepted in the batch. |
brand_code | string | The public code of the batch's brand. |
brand_id | integer | Deprecated: the brand's internal number, kept during the transition. Read brand_code. |
issue_purchase_codes | boolean | true when the batch was sent with purchase codes asked for. |
status: "queued" means that we have accepted the batch and that it is waiting for its
processing. We have processed nothing at that moment. Follow what comes next with
GET /v1/partner/mint/batch/status/{job_id}.
#Errors
The body of an error response carries a detail field. Depending on the case, this field
contains a sentence or an object.
A refusal that carries a sentence, here the empty batch, HTTP code 400:
{
"detail": "Batch vide (aucun item)"
}A refusal that carries an object, here an incomplete CSV header, HTTP code 400:
{
"detail": {
"error": "Colonnes manquantes",
"missing": ["category_id", "metadata_uri"]
}
}| Code | Condition | What to do |
|---|---|---|
| 400 | The JSON body sent is malformed. | Check that you are sending valid JSON, or a CSV file as multipart/form-data. |
| 400 | The JSON body is not a list at the top level. | Wrap your object in a list, even for a single item. |
| 400 | One or more JSON rows are invalid. detail is {"error": "JSON invalide", "rows": [...]}, each entry carrying the index of the row at fault and the reason. | Correct the rows reported. The first object of the list carries index 1. No row of the batch was processed. |
| 400 | The CSV file is empty or has no header line. detail is {"error": "CSV vide ou sans en-têtes"}. | Add the header line. |
| 400 | A required column is missing from the CSV header. detail is {"error": "Colonnes manquantes", "missing": [...]}. | Add the columns named in missing. The file was not read beyond its header. |
| 400 | One or more CSV rows are invalid. detail is {"error": "CSV invalide", "rows": [...]}, each entry carrying the line of the file, its external_ref and the reason. | Correct the rows reported. The first data line carries number 2. |
| 400 | The gzip file sent is unreadable. | Send the file again, or send it without compression. |
| 400 | The batch is empty. | Send at least one row. |
| 400 | The batch exceeds 500 rows. The message gives the number received and the cap. | Split your submission into several calls of 500 rows at most. |
| 400 | One or more category_id values match no category. The message lists the numbers that cannot be found. | Correct the category numbers. The list of categories is visible in the console. |
| 401 | The Authorization header is missing. The response also carries WWW-Authenticate: Bearer. | Add the header. |
| 401 | The Authorization header does not start with Bearer followed by a space. The response also carries WWW-Authenticate: Bearer. | Correct the form of the header. |
| 401 | The value sent after Bearer is empty or is shorter than 40 characters. | Send the complete secret, with no space and no line break. |
| 401 | The key sent is unknown. | Check that you are using a key of this environment, and that it has not been recreated. |
| 403 | The key is no longer active, because it was revoked. The message gives its state. | Create a new key in the console. |
| 403 | The key has reached its expiration date. | Create a new key. The old one will never become valid again. |
| 403 | The key does not carry the mint:batch scope. The message names the missing scope. | Create a key carrying this scope. The scopes of an existing key cannot be modified. |
| 403 | issue_purchase_codes=true and the key does not carry the claim_codes:issue scope. The message names the missing scope. Nothing is read or reserved. | Create a key carrying both scopes, or send the batch without the option. |
| 403 | The plan of your brand does not include API access. detail is {"code": "FEATURE_NOT_AVAILABLE", "feature": "api_access"}. | Contact us to open this right on your plan. Trying again will change nothing. |
| 403 | The plan of your brand does not allow identification by QR, which is the method set by this endpoint. detail carries the code AUTH_METHOD_NOT_ALLOWED. | Contact us to open QR on your plan. Trying again will change nothing. |
| 403 | At least one row carries a brand_code the key does not serve: neither its brand nor a client brand of your reseller contract. A code that names no brand gets the same answer. detail is "This API key cannot act for this brand.". | Correct the rows. No row of the batch was processed. |
| 403 | The subscription of the batch's brand is no longer active: trial over (detail is TRIAL_EXPIRED) or payment late or stopped (detail is SUBSCRIPTION_INACTIVE), or service suspended for an unpaid monthly invoice (detail is BILLING_SUSPENDED). The console refuses the same mint. | Bring that brand's subscription or invoice up to date. Nothing was reserved or queued. |
| 403 | On a trial account, the batch would exceed the 10 products of the billing period. detail is {"code": "QUOTA_EXCEEDED", "resource": "products", "current": ..., "additional": ..., "max": ..., "period": "monthly"}. | Wait for the next period, reduce the size of the batch, or subscribe to a plan. This refusal does not consume the daily quota of your key. A plan sold counts pieces per calendar year and prices those beyond the included ones: this refusal never happens for pieces. |
| 404 | A model_sku or a batch_code matches no model or batch of the brand of the key. detail.code is UNKNOWN_CATALOGUE_REFERENCE and detail.rows names each row and the field, never the value. Nothing is queued. | Create the model or the batch through the catalogue API, or correct the code. |
| 409 | The batch named by batch_code is already anchored on the blockchain. detail.code is BATCH_ALREADY_ANCHORED. Nothing is queued. | Create a new batch and mint on it. |
| 422 | The rows name several brands. detail.code is ONE_BRAND_PER_BATCH. | Send one batch per brand. |
| 422 | A JSON row carries a brand_code that does not have the shape of a public code, or a brand_id not written with digits only (5.0, "+5", true). detail.code is BRAND_REF_NOT_WELL_FORMED and detail.references gives the row and the field, never the value. In a CSV, the same fault is a row of the 400 CSV invalide refusal, whose message starts with BRAND_REF_NOT_WELL_FORMED. | Send the code as GET /v1/partner/catalog/brands gives it. Nothing was queued. |
| 422 | The batch named by batch_code belongs to another model than the model_sku of the same row. detail.code is BATCH_OF_ANOTHER_MODEL. | Send only batch_code, which brings its model, or correct the SKU. |
| 409 | The idempotency key has already been used for a different batch in the last 24 hours. | Use a new idempotency key for this batch. This batch was not processed. |
| 409 | A call carrying the same idempotency key is being processed. | Wait for the response of the first call, then read its result again. The lock expires after 60 seconds. |
| 413 | The compressed file exceeds about 20 MiB once decompressed. | Split your file. A batch of 500 rows stays well below this cap. |
| 429 | The rate cap of the key is reached. Retry-After and the X-RateLimit-* family come with the response, with X-RateLimit-Scope: key. | Wait the number of seconds given by Retry-After, then try again. |
| 429 | The rate cap of the brand is reached, across all keys. X-RateLimit-Scope is brand. | Wait the number of seconds given by Retry-After. Creating an additional key does not raise this cap. |
| 429 | The daily quota of the key is reached. The X-Quota-Limit, X-Quota-Remaining and X-Quota-Reset headers come with the response. | Wait for midnight universal time to pass, or have the quota of the key raised. The quota is consumed per item: a batch of 100 rows consumes 100 of it. |
| 500 | The CSV file sent is not encoded in UTF-8. | Save the file again in UTF-8. In a spreadsheet, choose the "CSV UTF-8" export. |
| 500 | Your JSON list contains an element that is not an object, for example a string or a number. | Send a list in which every element is an object carrying the fields described above. |
| 500 | A category_id outside the bounds the database accepts. | Send category numbers visible in the console. |
| 500 | The brand attached to your key cannot be found. | Contact us giving the time of the call. |
| 500 | An unexpected error occurred during the processing of your call. | Try again. If the error persists, contact support giving the time of the call. |
| 503 | The call-capping service is momentarily unavailable. We then refuse the call. | Try again in a few moments. No row was processed. |
#See also
GET /partner/mint/batch/status/{job_id}, follow the progress of a batch sent to minting.- Create products, one at a time and in batches, create a product, create a whole run, import a file.
- Partner API, overview, base address, keys, scopes, call caps and pagination.
- Issue purchase codes, one code per piece, shown once, at mint time or later.
- API errors, recognize an error code and decide whether to correct or replay.
Was this page helpful?
Your answer opens a pre-filled email in your mail app, addressed to contact@sealtrust.io. You read it over before sending it.