Integrating the TypeScript SDK
By the end of this page, you will know how to install the TypeScript SDK, create the client, read a product's history, manage your notification subscriptions, tell the two families of errors apart, and run a complete program.
On this page
- What the SDK covers
- Installing
- Creating the client
- Reading a product's history
- Two methods that refuse an API key
- Managing notification subscriptions
- Deleting a subscription requires its address
- Sending a batch to the partner API
- The idempotency key of this route
- Following the progress of a batch
- Telling the two families of errors apart
- Two different refusals carry the code 429
- Complete end-to-end example
- The known pitfalls, at a glance
The TypeScript SDK is a library published on npm under the name
@sealtrust-io/sdk. It sends the partner API calls on your behalf and returns
typed objects to you. By the end of this page, you will know how to install it,
create the client, read a product's history, manage your notification
subscriptions, tell the two families of errors apart, and run a complete program
that declares a subscription then reads a timeline.
You will also know how to create products in batches through the API, and what the server decides on your behalf on that path.
You first need an API key. You create it from your brand's console, in Settings then Developers, with the scopes your calls need.
#What the SDK covers
The SDK exposes three families of methods, and nothing else. Every row of the table below has been checked against the route it calls.
| Method | Route called | Scope required on the key |
|---|---|---|
products.mint() | POST /partner/mint/batch | mint:batch |
products.getBatchStatus() | GET /partner/mint/batch/status/{job_id} | mint:batch |
webhooks.create() | POST /partner/webhooks | webhooks:write |
webhooks.list() | GET /partner/webhooks | webhooks:read |
webhooks.get() | GET /partner/webhooks/{webhook_id} | webhooks:read |
webhooks.update() | PUT /partner/webhooks/{webhook_id} | webhooks:write |
webhooks.delete() | DELETE /partner/webhooks/{webhook_id} | webhooks:write |
verify.timeline() | GET /timeline/{identifier} | none, public route |
Two further methods exist in the SDK and refuse an API key: verify.batch() and
verify.metadataIntegrity(). The routes they call expect a console session. An
API key comes back from them with 401. They are described further down.
One route of the partner API has no method in the SDK: the declaration of a sale
to the end customer,
POST /partner/sellout, which requires
the sellout:write scope. Call it with fetch as long as the SDK does not
carry it.
#Installing
The latest published version is 0.3.0. The package requires Node 18 or more
recent, because it uses the language's native fetch.
npm install @sealtrust-io/sdkyarn add @sealtrust-io/sdkThe package gives you two formats, ES modules and CommonJS, with its type definitions.
The examples on this page use setTimeout and process, which belong to Node.
The package does not declare Node's types, so your compiler ignores them until
you install them.
npm install --save-dev @types/node#Creating the client
You build the client once, with your key, and you reuse it for all your calls.
import { SealTrustClient } from "@sealtrust-io/sdk";
const sealtrust = new SealTrustClient({
apiKey: process.env.SEALTRUST_API_KEY ?? "",
});You set the SEALTRUST_API_KEY variable in your server's environment. The name
is yours, the SDK reads no variable by itself. If the variable is absent, the
construction of the client fails immediately, before any call.
The constructor accepts four options.
| Option | Type | Required | Description |
|---|---|---|---|
apiKey | string | yes | Your API key. It goes out in the Authorization: Bearer <clef> header. An empty value makes the construction of the client fail immediately. |
baseUrl | string | no | The address of the API. Default value https://api.sealtrust.io. |
timeout | number | no | Maximum duration of a call, in milliseconds. Default value 30000. |
fetch | typeof fetch | no | The implementation of fetch to use. Default value the one of the language. |
The fetch option serves to test your own code without touching a global
variable. You pass it a function that returns the response of your choice.
const clientDeTest = new SealTrustClient({
apiKey: "clef-factice-du-test",
fetch: async () =>
new Response(JSON.stringify({ token_id: "1", timeline: [] }), {
status: 200,
headers: { "content-type": "application/json" },
}),
});Here the key does not matter: the fetch function you supply returns the
response of your choice and sends the request nowhere. The constructor only
requires that it not be empty.
#Reading a product's history
verify.timeline() returns the verifications and the ownership transfers of a
product, in a single object.
Three forms of identifier are accepted.
- The printed serial number, the one the QR code on the product carries. It is the only one a human can read on an object.
- The token identifier, in base ten.
- The UID hash,
0xfollowed by 64 hexadecimal characters.
const histoire = await sealtrust.verify.timeline("EXEMP1E00001");
console.log(histoire.product_name);
console.log(histoire.brand_name);
for (const evenement of histoire.timeline) {
console.log(evenement.type, evenement.timestamp);
}This route is public. Your API key is neither required nor read there, and the response is the same with or without it. It carries its own limit, 30 calls per 60 seconds and per calling address, independent of your key's quota. Beyond that, it answers 429. A well formed but unknown identifier answers 404.
The SDK refuses . and .. as identifiers and raises an error before any call.
These two values are not valid identifiers, and left as they are they would send
the request to an address other than the one requested.
#Two methods that refuse an API key
verify.batch() and verify.metadataIntegrity() call real routes. Those routes
expect a console session. An API key fails when that session is read and the
call comes back with 401. The code returned is the one of a refused
authentication, which gives the impression of a broken key.
Do not use these two methods with the key of your server integration.
#Managing notification subscriptions
The five webhooks methods cover all of the partner API's subscription routes.
const abonnement = await sealtrust.webhooks.create({
url: "https://exemple-sas.example/webhooks/sealtrust",
events: ["product.minted", "product.transferred"],
secret: "secret-de-demonstration-a-remplacer",
});
console.log(abonnement.id, abonnement.events, abonnement.health);The address must start with https://. The events field is optional in the
technical sense, and a subscription created without it never receives anything
while displaying itself in good health. Always fill it in.
The secret field is yours. The API records it as it is and signs every
delivery with it. If you do not send it, the deliveries go out unsigned.
The health field equals healthy as long as the deliveries succeed, and
degraded when the series of retries gives up on your address. It goes back to
healthy on the first successful delivery. It is the field to watch.
const page = await sealtrust.webhooks.list({ skip: 0, limit: 20 });
console.log(page.total);
for (const item of page.items) {
console.log(item.id, item.url, item.event_types, item.health);
}limit goes from 1 to 100, and equals 20 by default. The body of the response
carries total and items, and nothing else: the skip and limit values you
sent do not come back.
#Deleting a subscription requires its address
webhooks.delete() takes two arguments: the identifier, and the address of that
same subscription. The API compares the address you send with the one it has
recorded. If the two differ, nothing is deleted and the call fails with 400 and
the code CONFIRMATION_MISMATCH. With no address at all, the code is
CONFIRMATION_REQUIRED.
const cible = await sealtrust.webhooks.get(7);
await sealtrust.webhooks.delete(cible.id, cible.url);Deleting a subscription also erases the recorded signing secret. You will have to send it again at the next creation, and until you have done so, the deliveries go out unsigned. There is no undoing this.
The SDK does not go and fetch the address on your behalf, deliberately. The check exists to catch an identifier that does not designate what you think, and a read made on that same identifier would confirm the wrong subscription just as well as the right one.
The error message never contains the expected address. Read it with
webhooks.get(id).
#Sending a batch to the partner API
This endpoint creates products. The server itself sets two values on each row:
the identification method, which equals qr, and the item's UID hash. Your row
does not carry them and cannot carry them, the request schema refuses any field
it does not know.
products.mint() accepts a single object or an array of objects. The SDK wraps
the single object in an array before sending, so the result is identical.
A row accepts five fields, and five only.
| Field | Type | Required | Description |
|---|---|---|---|
product_name | string | yes | The product's name. |
brand_id | number | yes | Your brand's number. It must match the brand of the key. |
category_id | number | yes | The category's number. |
metadata_uri | string | yes | The address of the product's metadata. |
external_ref | string | no | Your own reference. |
A batch counts 500 items at most. A single row carrying the number of another brand causes the whole batch to be refused with 403.
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",
},
]);
console.log(lot.job_id, lot.status, lot.items_count, lot.brand_id);Four conditions come on top of the key's mint:batch scope, in this order.
- Your brand's plan must include access to the API. Otherwise the response is
403 with the code
FEATURE_NOT_AVAILABLE. - Your brand's plan must allow the
qrmethod. Otherwise the whole batch is refused with 403 and the codeAUTH_METHOD_NOT_ALLOWED. - Your brand's monthly product allowance must be able to absorb the whole batch.
Otherwise the response is 403 with the code
QUOTA_EXCEEDED, and the body givescurrent,additional,maxandperiod. - The key's daily quota must remain sufficient. It counts one item per row. A batch of 200 items consumes 200 quota units. Otherwise the response is 429. The quota is consumed at the moment the batch is accepted, and it is not given back to you if the processing fails afterwards.
#The idempotency key of this route
POST /v1/partner/mint/batch is the only route of the partner API that reads
the Idempotency-Key header. The SDK sets that header on every POST and every
PUT, and all the other routes ignore it.
When you do not supply one, the SDK makes one up at random, which protects you from nothing: the SDK never retries by itself, one call equals one send, and a randomly drawn key changes at every send.
So that a new attempt after a network outage finds the first batch again instead
of queuing a second one, supply your own key, and keep the same one between
attempts. products.mint() takes it as a second argument.
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",
},
],
"exemple-lot-2026-08-20-001",
);The API keeps your idempotency key for 24 hours. Replaying the same key with the same batch returns the response of the first call. Replaying the same key with a different batch returns 409.
#Following the progress of a batch
products.getBatchStatus() takes the identifier returned by products.mint().
The status field equals queued when waiting, started when in progress,
finished when done, failed when failed, and unknown when the batch cannot
be found. The type declares four other values that the execution queue can
produce, which prevents an exhaustive switch from refusing to compile.
const etat = await sealtrust.products.getBatchStatus("9f2c4a7b1d3e5f60");
console.log(etat.status);Loop as long as the status equals queued or started.
When the queue has forgotten a finished batch but the reservation still exists
and belongs to you, the response carries four extra fields: batch_status,
items_count, success_count and error_count. They are absent from all the
other responses, which is why they are optional in the type.
#Telling the two families of errors apart
The SDK raises two error classes, and the difference calls for opposite reactions.
| Class | What happened | What you have |
|---|---|---|
SealTrustError | The API answered and refused. | status, body, headers. |
SealTrustNetworkError | The API could not be reached, or its response could not be read. | cause. |
SealTrustNetworkError covers three cases: transport failure, timeout exceeded,
and a body announced as JSON that is not JSON, including an empty body on a 200
response.
import {
SealTrustClient,
SealTrustError,
SealTrustNetworkError,
} from "@sealtrust-io/sdk";
const sealtrust = new SealTrustClient({
apiKey: process.env.SEALTRUST_API_KEY ?? "",
});
try {
await sealtrust.verify.timeline("EXEMP1E00001");
} catch (erreur) {
if (erreur instanceof SealTrustError) {
console.error(erreur.status, erreur.message);
console.error(erreur.headers.get("x-request-id"));
} else if (erreur instanceof SealTrustNetworkError) {
console.error("API injoignable :", erreur.message);
} else {
throw erreur;
}
}The API's error body carries a detail field. It is a string in most refusals,
an object when the route refuses in a structured way, and an array of fields
when validation fails with 422. When it is an object, it carries a code field:
FEATURE_NOT_AVAILABLE, AUTH_METHOD_NOT_ALLOWED, QUOTA_EXCEEDED,
CONFIRMATION_MISMATCH. Base your logic on that code. The message, for its
part, can change without notice.
import { SealTrustError } from "@sealtrust-io/sdk";
function codeDErreur(erreur: SealTrustError): string | null {
const detail = erreur.body?.detail;
if (detail && typeof detail === "object" && !Array.isArray(detail)) {
const code = (detail as Record<string, unknown>).code;
return typeof code === "string" ? code : null;
}
return null;
}The request number travels in the X-Request-Id header, readable from
erreur.headers.
#Two different refusals carry the code 429
A 429 does not mean the same thing depending on what produced it, and the header tells you which one you have.
| Refusal | Headers | When to retry |
|---|---|---|
| Rate limit | Retry-After, plus X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and X-RateLimit-Scope | after the number of seconds given by Retry-After |
| Daily quota of the key | X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset. No Retry-After | after midnight UTC, when the quota starts again |
The daily quota can refuse you on two routes only, the ones that consume items:
products.mint() and the sale declaration. The rate limit, for its part,
applies to every route that reads your key. X-Quota-Reset carries the
timestamp of the last reset, it does not give the next one.
Retrying right away after a quota refusal fails the same way. The function below
therefore waits in the first case, and returns false for the second.
import { SealTrustError } from "@sealtrust-io/sdk";
async function attendreSiPlafondDeDebit(erreur: unknown): Promise<boolean> {
if (!(erreur instanceof SealTrustError) || erreur.status !== 429) {
return false;
}
const retryAfter = erreur.headers.get("retry-after");
if (retryAfter === null) {
console.error(
"Quota quotidien atteint :",
erreur.headers.get("x-quota-remaining"),
"sur",
erreur.headers.get("x-quota-limit"),
"Il repart à minuit UTC.",
);
return false;
}
const secondes = Number(retryAfter);
await new Promise((resoudre) => setTimeout(resoudre, secondes * 1000));
return true;
}#Complete end-to-end example
This program does two things in a row: it declares a notification subscription
if you do not have one on that address yet, then it reads a product's history.
It compiles and runs as it is with Node 18 or more recent and @types/node
installed, once the SEALTRUST_API_KEY variable is set in your environment, and
the address as well as the product identifier replaced with yours.
Declaring a subscription requires a plan that includes notifications. If your
plan does not include them, the call answers 403 FEATURE_NOT_AVAILABLE, the
program says so and carries on.
import {
SealTrustClient,
SealTrustError,
SealTrustNetworkError,
} from "@sealtrust-io/sdk";
const CLEF = process.env.SEALTRUST_API_KEY ?? "";
const ADRESSE_NOTIFICATION = "https://exemple-sas.example/webhooks/sealtrust";
const SECRET_NOTIFICATION = "secret-de-demonstration-a-remplacer";
const PRODUIT = "EXEMP1E00001";
const sealtrust = new SealTrustClient({ apiKey: CLEF });
function codeDErreur(erreur: SealTrustError): string | null {
const detail = erreur.body?.detail;
if (detail && typeof detail === "object" && !Array.isArray(detail)) {
const code = (detail as Record<string, unknown>).code;
return typeof code === "string" ? code : null;
}
return null;
}
async function chercherAbonnement(): Promise<number | null> {
let vus = 0;
for (;;) {
const page = await sealtrust.webhooks.list({ skip: vus, limit: 100 });
const trouve = page.items.find((item) => item.url === ADRESSE_NOTIFICATION);
if (trouve) {
return trouve.id;
}
vus += page.items.length;
if (page.items.length === 0 || vus >= page.total) {
return null;
}
}
}
async function declarerAbonnement(): Promise<number | null> {
const existant = await chercherAbonnement();
if (existant !== null) {
console.log("Abonnement déjà présent :", existant);
return existant;
}
try {
const cree = await sealtrust.webhooks.create({
url: ADRESSE_NOTIFICATION,
events: ["product.minted", "product.transferred"],
secret: SECRET_NOTIFICATION,
});
console.log("Abonnement créé :", cree.id, cree.events.join(", "));
return cree.id;
} catch (erreur) {
if (
erreur instanceof SealTrustError &&
codeDErreur(erreur) === "FEATURE_NOT_AVAILABLE"
) {
console.error("Offre sans notifications : abonnement non déclaré.");
return null;
}
throw erreur;
}
}
async function lireHistorique(): Promise<void> {
const histoire = await sealtrust.verify.timeline(PRODUIT);
console.log("Produit :", histoire.product_name);
console.log("Marque :", histoire.brand_name);
console.log("Événements :", histoire.timeline.length);
for (const evenement of histoire.timeline) {
console.log(" ", evenement.type, evenement.timestamp);
}
}
async function principal(): Promise<void> {
await declarerAbonnement();
await lireHistorique();
}
principal().catch((erreur) => {
if (erreur instanceof SealTrustError) {
console.error(`L'API a refusé en ${erreur.status} :`, erreur.message);
console.error("Numéro de requête :", erreur.headers.get("x-request-id"));
} else if (erreur instanceof SealTrustNetworkError) {
console.error("API injoignable :", erreur.message);
} else {
console.error(erreur);
}
process.exitCode = 1;
});Reading the list before creating is deliberate. Without it, a program restarted after an outage declares a second subscription on the same address, and your receiving server gets every event twice.
#The known pitfalls, at a glance
| What you see | Cause | What to do |
|---|---|---|
a finished batch without knowing how many items went through | this status describes the execution queue, it does not count the items | open the list of products in the console, the SDK exposes no method that reads it |
| 400 on a batch that the compiler accepts | you are sending owner_email or contract_address, still declared in the type and refused by the API | remove those two fields |
401 on verify.batch() or verify.metadataIntegrity() | these routes expect a console session | do not use these two methods with an API key |
| 403 with scopes named in the message | the key does not have the scope required by the route | create a new key carrying the named scope, from the console |
| 403 on the whole batch | at least one row carries a brand_id different from the one of the key | correct the offending row, the whole batch is refused because of a single one |
403 FEATURE_NOT_AVAILABLE | your plan does not include the function called: access to the API, or notifications on a creation and on a modification of a subscription | check your plan in the console |
403 AUTH_METHOD_NOT_ALLOWED on a batch | your plan does not allow the qr method, which the server sets on every row of this endpoint | check your plan in the console |
403 QUOTA_EXCEEDED | your brand's monthly product allowance is reached, and this batch would go past it | reduce the size of the batch or wait for the next period; the body gives current, additional, max and period |
products.list is not a function, or sealtrust.certificates equals undefined | you are calling a method that is absent from the SDK: products.list(), products.get(), verify.product(), or the certificates family | no published version of the package has ever carried them, and the corresponding routes have never existed |
undefined when reading hook.events on a list item | the list returns event_types | read item.event_types on the items of list() |
| every event delivered twice | two subscriptions carry the same address, a create() was replayed | list your subscriptions and delete the duplicate |
| a waiting loop that never stops | you are testing is_finished, absent from the unknown response | loop on status, and stop as soon as it leaves queued and started |
400 CONFIRMATION_MISMATCH on deletion | the address sent does not match the one recorded under this identifier | read the subscription again with webhooks.get(id) and pass its url |
429 with Retry-After | the rate limit is reached | wait the number of seconds given by Retry-After |
429 without Retry-After | the key's daily quota is reached | wait for midnight UTC; retrying before then fails the same way |
| 503 | the rate limiting service is momentarily unavailable | retry shortly after, nothing has been processed |
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.