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.
On this page
- What you need
- The ten endpoints
- Naming the brand
- Four rules hold for every write
- Create or complete a model
- Create or complete a batch
- Create or complete the draft passport
- The passport of a batch
- A brand's offer
- The code to print
- Read the completeness score
- One key for several brands: the reseller key
- Mint, sell and receive webhooks for a client brand
- Limits
- Errors
- See also
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:writescope to write, andcatalog:readto 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.
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.
{
"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.
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.
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.
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:
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, withpiecesandbatches_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) andpieces_kept. The change is recorded in the journal, in the key's name.
The two offers are detailed on the offers page.
#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:
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_pngandqr_png_downloadthen give the address of the image, to open without a key.- Otherwise
reasonsays 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_GTINorLOT_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.
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, 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. 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, keys, scopes and limits.
- TypeScript SDK, which does not cover these endpoints in its published version yet.
- API keys and webhooks, create the key in the console.
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.