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

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 pathWhat it doesScope
GET /partner/catalog/brandsthe brands your key may act forcatalog:read or catalog:write
PUT /partner/catalog/brands/{brand_code}/modelscreates or completes a model, found by its SKUcatalog:write
GET /partner/catalog/brands/{brand_code}/modelsone model with ?sku=, otherwise the list of modelscatalog:read
GET /partner/catalog/brands/{brand_code}/readiness?sku=the completeness score of one modelcatalog:read
PUT /partner/catalog/brands/{brand_code}/batchescreates or completes a batch, found by its codecatalog:write
PUT /partner/catalog/brands/{brand_code}/passportscreates or completes the draft passport of a model, or of one of its batches with batch_codecatalog: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}/offerthe brand's offer: Compliance, or Compliance + Identitycatalog:read or catalog:write
PUT /partner/catalog/brands/{brand_code}/offerchanges the offer of one of your client brandscatalog:write
GET /partner/catalog/brands/{brand_code}/codes?sku=the link and the QR code to print, for a model or a batchcatalog: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
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.

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.

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.

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:

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.

#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
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.

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, 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

CodeConditionWhat to do
401Key missing, malformed or unknown.Send Authorization: Bearer <your key>.
403The scope is missing. The message names it, for example Missing required scope: catalog:write.Create a key carrying that scope.
403Your 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.
403The plan does not include API access, or not the digital passport. detail carries FEATURE_NOT_AVAILABLE.Contact us to change plan.
403Creating 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.
404No model of this brand carries this SKU, or the model has no passport yet.Create the model first.
404Unknown category_id.Use an existing category identifier.
409The batch belongs to another model, or it is anchored on chain.Make that change in the console.
409The new GTIN is already published by another brand.Check the GTIN.
409One 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.
409Two writes opened a version of the same passport at the same moment, twice in a row. This call wrote nothing.Send the request again.
409OFFER_CHANGE_NEEDS_CONFIRMATION: the brand has pieces, or mint batches not finished.Send again with "confirm_pieces_kept": true if that is intended.
422UNKNOWN_OFFER: the requested offer does not exist.Send compliance or compliance_identity.
422Invalid 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.
429Rate limit or daily quota reached.Wait for the delay given by Retry-After, or for midnight in universal time for the quota.
503Call counting is momentarily unavailable. Nothing was written.Try again in a few seconds.

#See also

Your answer opens a pre-filled email in your mail app, addressed to contact@sealtrust.io. You read it over before sending it.

Suggest a correctionReport a problem