# Fill the catalog through the API

Create or complete models, batches and draft passports from your platform, read the completeness score, and act for several brands with a reseller key. Scopes catalog:read and catalog:write.

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

---

Your platform already knows your products. This page explains how to send them
into SealTrust without anyone typing them again: the models, the production
batches and the draft passport of each model. You send what you have. The brand
completes the rest in the console, then publishes.

## What you need

- An API key carrying the `catalog:write` scope to write, and `catalog:read` to
  read back. You create it in the console, Settings then Developers.
- A plan that includes API access. For passports, the plan of the target brand
  must also include the digital product passport.

## The ten endpoints

Every address names the target brand in its path. All of them also exist with
the `/v1` prefix, which is the recommended form.

| Method and path | What it does | Scope |
| --- | --- | --- |
| `GET /partner/catalog/brands` | the brands your key may act for | `catalog:read` or `catalog:write` |
| `PUT /partner/catalog/brands/{brand_code}/models` | creates or completes a model, found by its SKU | `catalog:write` |
| `GET /partner/catalog/brands/{brand_code}/models` | one model with `?sku=`, otherwise the list of models | `catalog:read` |
| `GET /partner/catalog/brands/{brand_code}/readiness?sku=` | the completeness score of one model | `catalog:read` |
| `PUT /partner/catalog/brands/{brand_code}/batches` | creates or completes a batch, found by its code | `catalog:write` |
| `PUT /partner/catalog/brands/{brand_code}/passports` | creates or completes the draft passport of a model, or of one of its batches with `batch_code` | `catalog:write` |
| `GET /partner/catalog/brands/{brand_code}/passports?sku=` | the latest version of this model's passport, or of one of its batches with `&batch_code=` | `catalog:read` |
| `GET /partner/catalog/brands/{brand_code}/offer` | the brand's offer: Compliance, or Compliance + Identity | `catalog:read` or `catalog:write` |
| `PUT /partner/catalog/brands/{brand_code}/offer` | changes the offer of one of your client brands | `catalog:write` |
| `GET /partner/catalog/brands/{brand_code}/codes?sku=` | the link and the QR code to print, for a model or a batch | `catalog:read` |

The SKU travels in the body or in the query, never in the path: a SKU may
contain a slash.

## Naming the brand

Every brand has a public code of ten characters: `brand_code`, for example
`7K3QXW9M2A`. This code is made of digits and letters, without I, L, O or U.
Upper case and lower case are the same. `GET /partner/catalog/brands` gives
the code of every brand your key may act for. Put this code in the path.

The answers also carry `brand_id`, the brand's internal number. This field is
deprecated: read `brand_code`. A number sent in place of the code is still
accepted for now, but build nothing on it.

## Four rules hold for every write

**Replaying changes nothing.** A model's SKU and a batch's code are the keys.
Sending the same thing twice leaves the same state. The second answer carries
`changed: false`. After a network cut, replay without worry: no
`Idempotency-Key` header is needed, and no duplicate is possible. This also
holds when your retry arrives while the first call is still running: a single
draft is opened.

**A field left out erases nothing, a list you send replaces the list.** A field
left out, or sent as `null`, keeps its value, and an object is completed key by
key: what the brand entered in the other keys stays in place. A list, however,
is replaced whole. If the brand added a supplier in the console and your next
push contains `suppliers`, your list is what remains, and the brand's addition
is gone. Before sending a list again, read the draft with
`GET /partner/catalog/brands/{brand_code}/passports?sku=` and send the complete
list, or do not send the list. To empty a field, use the console.

**Nothing is published, nothing sealed is rewritten.** A passport sent through
the API stays a private draft. Publishing remains the brand's act, in the
console. If the brand has already sealed a version, your next push opens a new
draft version on top of it: the sealed version never moves.

**Every write leaves a trace.** SealTrust records every write in its audit
log, as it does for a change made in the console. The log names the key that
wrote, the brand it acted for, and each changed field with its value before
and after. A model write also appears in that model's history in the console,
with the origin "API" and the key's prefix as its author. A write that changes
nothing leaves nothing there. Every call, reads included, is also recorded in
the key's usage log, with the target brand in its path.

## Create or complete a model

The body accepts the fields of a console model. `sku` is required. `name` is
required only on creation. A GTIN whose check digit is wrong is refused with a
422, and so is any unknown field.

`primary_image_url` is the image we copy to IPFS when the pieces are created. It must be an `http` or `https` address on a public server. A private network address, a local address or a name that is not a full domain name is refused with a 422. If the name points to a private address when the pieces are created, the image is not copied and the piece is created anyway.

`primary_image_url` may also be a key of the target brand's media library, of
the form `brands/{id}/…`. A key of another brand is refused with a 422. Any
value that is neither an address nor a key, for example
`javascript:alert(1)` or free text, is refused with a 422 as well.

`weight_grams`, `volume_cm3` and `default_warranty_months` are 0 or more. A
negative value is refused with a 422.

The six instruction links (`repair_instructions_url`,
`maintenance_instructions_url`, `safety_instructions_url`, `user_manual_url`,
`spare_parts_url`, `disassembly_instructions_url`) are full web addresses
starting with `https://` or `http://`. A relative path, free text, or a
`javascript:` or `data:` address is refused with a 422.

`identity_level` is `unit`: each piece gets its own token and its own QR code,
for authentication, owner, resale and theft report, and its page shows the
passport of its batch, or else of the model. For a piece minted through
`POST /v1/partner/mint/batch` to be attached that way, name `model_sku` on its
row, and `batch_code` if it belongs to a batch. A model created without this field
is at `unit` level, as through every other path. Sending `model` answers `422`.
A push without this field never changes the level of an existing model.

```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"}'
```

The answer is 201 on creation, 200 otherwise.

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

The returned model carries all its fields. This example shows only five.

The passport template, and so the readiness score, follows from the model's
category (`category_id`). For a battery, pick the "Battery" category: the model
is then scored against Regulation (EU) 2023/1542 on batteries, not against
consumer electronics. You can also name the template yourself with
`playbook_key`, for example `"playbook_key": "battery"`. This field takes
precedence over the category. An unknown value is refused with 422.

## Create or complete a batch

`batch_code` and `model_sku` are required. The SKU names a model of the same
brand. The other fields are `production_date` (a date in `YYYY-MM-DD` form),
`manufacturing_site`, `country_of_origin` (two letters), `quantity_planned` and
`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"}'
```

Two refusals protect what is already settled. A batch does not change model
through the API. A batch anchored on chain does not change at all any more: its
facts are published.

The returned batch carries `lot_passport_blocker`. It is `null` when the batch
code can carry a lot passport. Otherwise it says why: the link
`/01/{gtin}/10/{lot}` requires a GS1 code of 20 characters at most, from a
restricted set (letters, digits and a few signs, no space and no slash). The batch is
created all the same, because its code is also your production reference. It
will simply never have a lot passport: choose a shorter code if you want one.

## Create or complete the draft passport

`sku` names the model. `data` follows the structure of the console passport. We
merge `data` into the latest version: objects are completed key by key,
anything else replaces, and `null` keeps the value in 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"}}}'
```

The answer carries the passport, with `status` set to `draft`, and
`visibility` set to `brand_only`. There is no field to publish: sending one is
refused with a 422.

### The passport of a batch

Add `batch_code` to write the passport of a batch of that model rather than the
model's. It is the one that answers the link `/01/{gtin}/10/{lot}`. A first
version starts from the model's published passport, completed with the batch's
information. The GTIN and the batch number are always written into it: data
naming another batch is refused with a 422, never overwritten. The batch must
belong to the model named by `sku`, otherwise the answer is 404. To read this
passport back, add `&batch_code=` to the read.

```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}}}'
```

The answer carries `batch_code` and `product_batch_id`. `product_model_id` is
`null` in it: a batch passport is attached to its batch.

## A brand's offer

Each brand is on the Compliance offer (`compliance`) or on the Compliance +
Identity offer (`compliance_identity`). The list of brands carries each one's
offer. `GET .../offer` reads it back.

With a reseller key, `PUT .../offer` changes the offer of one of your client
brands, with the same rules as the 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"}'
```

- A brand never changes its own offer, and neither does a reseller: the answer
  is the same 403 as for a brand out of the key's reach.
- Leaving the Compliance + Identity offer while the brand has pieces, or mint
  batches not finished, answers 409 `OFFER_CHANGE_NEEDS_CONFIRMATION`, with
  `pieces` and `batches_in_flight`. The pieces are kept, but their identity
  operations stop, and a batch not finished creates no piece. Send again with
  `"confirm_pieces_kept": true`.
- An unknown offer answers 422 `UNKNOWN_OFFER`.
- The answer says `changed` (false when the brand already had this offer) and
  `pieces_kept`. The change is recorded in the journal, in the key's name.

The two offers are detailed on [the offers page](/en/offres-conformite-et-identite/).

## The code to print

A brand on the Compliance offer prints the same code on every product of a
model, or of a batch. `GET .../codes?sku=` gives it for a model, `&batch_code=`
for a batch:

```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`: the GS1 link the code carries, on the brand's domain once it is
  locked.
- `printable`: true when the code can be printed. `qr_png` and
  `qr_png_download` then give the address of the image, to open without a key.
- Otherwise `reason` says why: `ITEM_QR_ONLY` (the brand is on the Compliance +
  Identity offer, each piece carries its own code), `PASSPORT_NOT_PUBLISHED`
  (the brand first publishes this passport in the console), `MODEL_HAS_NO_GTIN`
  or `LOT_CODE_NOT_ENCODABLE` (no GS1 link is possible).

## Read the completeness score

It is the score the console shows, from 0 to 100, with the detail of what is
missing.

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

## One key for several brands: the reseller key

An ordinary key acts for its brand, and for it alone.

A **reseller key** acts for the reseller's brand and for each of the client
brands attached to its reseller contract. You name the target brand on every
call, in the path. `GET /partner/catalog/brands` returns the list of allowed
brands, yours first.

It does for a client brand everything your contract allows you: the catalog,
the offer, the codes to print,
[the passport display](/en/api-affichage-partenaire/), minting pieces, sale
declarations and webhooks.

What the reseller key does not do:

- It reaches no brand that is not attached to your contract. A brand of another
  reseller, a brand with no link, or a code that names no brand all receive
  the same 403.
- It follows your contract, read again on every call. Without a contract, it
  only reaches your own brand.

The "Reseller key" box is ticked when the key is created, in the console. It is
refused for a brand that has no reseller contract.

### Mint, sell and receive webhooks for a client brand

With your reseller key, you mint the pieces of a client brand with no extra
key: put the client brand's code in the `brand_code` of every row of
`POST /v1/partner/mint/batch`. All the rows of a batch carry the same brand. A
batch that mixes several is refused with 422 `ONE_BRAND_PER_BATCH`: send one
batch per brand. The batch status and the list of its pieces, with the link to
print, are read with the same key.

To declare a sale, add `brand_code` to the body of `POST /v1/partner/sellout`.
For a webhook, add `brand_code` to the body of `POST /v1/partner/webhooks`, and
to the list: `GET /v1/partner/webhooks?brand_code=7K3QXW9M2A`. Without
`brand_code`, the key acts for your own brand. Do not send `brand_code` and
`brand_id` together: the request is refused with 422.

The client brand's rules apply: its offer (on the Compliance offer the brand
has no pieces and minting is refused), its pieces of the year and its right to
webhooks. API access comes from your reseller offer. Every write is recorded
with the target brand and your key.

A webhook you create for a client brand, through the API or in the console, is
sent only while your reseller contract exists and the brand is still your
client. When either one stops, your address receives nothing more from that
brand, retries included. Changing keys changes nothing: your webhooks keep
arriving.

A key created on the client brand itself, in the console, still works, for that
brand alone.

## Limits

The rate limit is the one of the whole partner API, described in the
[overview](/en/api-vue-ensemble/). The daily quota of the key counts one unit
per write that creates or changes something. A replayed write that changes
nothing costs nothing.

## Errors

| Code | Condition | What to do |
| --- | --- | --- |
| 401 | Key missing, malformed or unknown. | Send `Authorization: Bearer <your key>`. |
| 403 | The scope is missing. The message names it, for example `Missing required scope: catalog:write`. | Create a key carrying that scope. |
| 403 | Your key cannot act for this brand. Message `This API key cannot act for this brand.` | Check the brand's code with `GET /partner/catalog/brands`. |
| 403 | The plan does not include API access, or not the digital passport. `detail` carries `FEATURE_NOT_AVAILABLE`. | Contact us to change plan. |
| 403 | Creating a model or a lot is suspended for an unpaid monthly invoice. `detail` carries `BILLING_SUSPENDED`. Passports already published stay online, and updating an existing model or lot still works. | Pay the invoice. The suspension is lifted as soon as it is paid. |
| 404 | No model of this brand carries this SKU, or the model has no passport yet. | Create the model first. |
| 404 | Unknown `category_id`. | Use an existing category identifier. |
| 409 | The batch belongs to another model, or it is anchored on chain. | Make that change in the console. |
| 409 | The new GTIN is already published by another brand. | Check the GTIN. |
| 409 | One of this model's batches has a published batch passport: the GTIN is printed in its GS1 link and sealed in its data, so it no longer changes. | Keep the current GTIN. |
| 409 | Two writes opened a version of the same passport at the same moment, twice in a row. This call wrote nothing. | Send the request again. |
| 409 | `OFFER_CHANGE_NEEDS_CONFIRMATION`: the brand has pieces, or mint batches not finished. | Send again with `"confirm_pieces_kept": true` if that is intended. |
| 422 | `UNKNOWN_OFFER`: the requested offer does not exist. | Send `compliance` or `compliance_identity`. |
| 422 | Invalid body: unknown field, `name` missing on creation, GTIN with a wrong check digit, image outside a public server or image key of another brand, negative weight, volume or warranty, instruction link that is not a web address, country that is not two letters. | The body of the answer names the faulty field. |
| 429 | Rate limit or daily quota reached. | Wait for the delay given by `Retry-After`, or for midnight in universal time for the quota. |
| 503 | Call counting is momentarily unavailable. Nothing was written. | Try again in a few seconds. |

## See also

- [Partner API overview](/en/api-vue-ensemble/), keys, scopes and limits.
- [TypeScript SDK](/en/sdk-typescript/), which does not cover these endpoints
  in its published version yet.
- [API keys and webhooks](/en/console/reglages-developpeurs/), create the key in
  the console.
