Method GET/resolve /{identifier}
Read in a single call everything a product page displays: identity, certificate, public passport, media, history and anchoring proofs. Public endpoint, no API key.
On this page
You obtain in a single call everything a product page displays: the identity of the item, its certificate in force, its published passport, its media, its history and its proofs of anchoring on the chain. No API key is required.
The full address is https://api.sealtrust.io/v1/resolve/{identifier}. The
same route exists without the /v1 prefix, and the /v1 form is the one
recommended for a new integration.
A single parameter is enough: the identifier of the item. Four forms are accepted, and you do not have to declare which one you send. The server tries them in order.
#
None, this is a public endpoint. It expects no API key, no session cookie, and
no Authorization header. A server-to-server call is accepted.
#Rate limit
60 calls per 60 second window, counted per calling IP address. The limit is
shared by every address that starts with /resolve, and it applies to
/resolve/{identifier} as much as to /v1/resolve/{identifier}. The counter
is common to every identifier value: going through a thousand different
identifiers consumes a thousand calls from the same budget.
The 200, 404, 405 and 429 responses carry the headers X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset, the last one giving the reset
time in seconds since January 1, 1970. A 500 response does not carry them.
Always read them with a fallback value. Going over returns 429 with
Retry-After in addition, in seconds.
This endpoint consumes no quota of your plan.
Every response carries the header Cache-Control: no-store, max-age=0. Do not
put this response in any shared cache. To reduce the number of your calls,
keep the result in your own application cache, with the freshness window your
use tolerates.
#Path and query parameters
| Name | Type | Required | Description |
|---|---|---|---|
identifier | string | yes | The identifier of the item. Four accepted forms, described below. The server strips the edge spaces before the search. |
This endpoint has no query parameter.
#The four forms of identifier
The server tries them in this order and stops at the first one that finds an item.
| Order | Form | Recognized by | Case sensitive |
|---|---|---|---|
| 1 | Hash of the item | 0x followed by 64 hexadecimal characters, that is 66 characters | no |
| 2 | Identifier of the token | Any value, compared as it stands to the recorded token identifier | yes |
| 3 | Printed serial number | 12 characters of the Crockford Base32 alphabet, which excludes the letters I, L, O and U | no |
| 4 | Certificate number | The exact value of the certificate_number field, for example ST-CERT-000000000000 | yes |
The serial number is the one the QR code printed on the item carries. The
server canonicalizes it before the search: it folds the letters I and L
into the digit 1, and the letter O into the digit 0. So the server still
recognizes a number retyped by hand with an I, an L or an O in place of
a 1 or a 0.
A form that finds nothing does not stop the search. A value of 66 characters
starting with 0x that matches no hash is then tried as a token identifier,
then as a serial number, then as a certificate number, before the 404.
Only the items still in the catalog answer. The server treats as not found a destroyed item, an item replaced by a later mint, and an archived item.
If several records match a hash, a token identifier or a serial number, the server returns the most recently created one. A certificate number is unique, it designates a single item.
#Request body
None. This is a GET request, everything goes through the path.
#Example request
curl -i https://api.sealtrust.io/v1/resolve/0x0000000000000000000000000000000000000000000000000000000000000000const identifiant =
"0x0000000000000000000000000000000000000000000000000000000000000000";
const response = await fetch(
`https://api.sealtrust.io/v1/resolve/${encodeURIComponent(identifiant)}`,
);
console.log(response.status);
console.log(response.headers.get("X-RateLimit-Remaining") ?? "inconnu");
console.log(await response.json());import requests
from urllib.parse import quote
identifiant = "0x0000000000000000000000000000000000000000000000000000000000000000"
response = requests.get(
f"https://api.sealtrust.io/v1/resolve/{quote(identifiant, safe='')}",
timeout=30,
)
print(response.status_code)
print(response.headers.get("X-RateLimit-Remaining", "inconnu"))
print(response.json())#Example response
HTTP code 200. A complete item, with a certificate, a published passport, one media item, one event and both proofs of anchoring.
{
"token_id": "1024",
"uid_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"product_name": "Sac de voyage Exemple SAS",
"brand_name": "Exemple SAS",
"category_name": "Maroquinerie",
"sku": null,
"description": null,
"metadata_uri": "ipfs://bafyexemple00000000000000000000000000000000000000000000000",
"created_at": "2026-08-14T09:12:33.418000Z",
"tx_hash": "0x4444444444444444444444444444444444444444444444444444444444444444",
"contract_address": "0x0000000000000000000000000000000000000000",
"certificate": {
"certificate_number": "ST-CERT-000000000000",
"status": "active",
"issued_at": "2026-08-14T09:14:02.117043Z",
"expires_at": null,
"issuer_name": "Exemple SAS"
},
"passport": {
"schema_version": "1.0",
"passport_version": 3,
"data": {
"product_identity": {
"gtin": "03701234567890",
"model": "Sac de voyage",
"brand": "Exemple SAS",
"made_in": "FR",
"production_facility": "Atelier Exemple SAS"
},
"materials": {
"primary": {
"name": "Full grain leather",
"percentage": 70,
"origin": "IT"
},
"certified_organic": false
},
"environmental_impact": {
"carbon_footprint_kg_co2e": 18.7,
"water_usage_liters": 2340,
"energy_consumption_kwh": 45.2,
"transport_distance_km": 850,
"transport_mode": "road"
},
"circularity": {
"recyclability_percentage": 62,
"recycled_content_percentage": 0,
"repairability_index": 7.8,
"expected_lifetime_years": 15,
"disassembly_instructions_url": "",
"take_back_program": true
},
"compliance": {
"eu_espr": true,
"reach": true
}
},
"published_at": "2026-08-18T07:03:11.902000Z",
"data_hash": "0000000000000000000000000000000000000000000000000000000000000000"
},
"media": [
{
"id": 4821,
"file_name": "sac-face.jpg",
"media_type": "image",
"url": "https://cdn.example.com/exemple-sas/sac-face.jpg",
"alt_text": "Sac de voyage vu de face"
}
],
"events": [
{
"id": 9137,
"event_type": "warranty_activation",
"description": "Garantie activée à l'achat en boutique.",
"occurred_at": "2026-08-19T14:32:07.481920Z",
"actor_name": "Boutique Exemple SAS Lyon"
}
],
"merkle_anchor": {
"anchor_tx_hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
"root": "0x2222222222222222222222222222222222222222222222222222222222222222",
"leaf_index": 17,
"leaf": "0x3333333333333333333333333333333333333333333333333333333333333333",
"proof": [
"0x5555555555555555555555555555555555555555555555555555555555555555",
"0x6666666666666666666666666666666666666666666666666666666666666666"
]
},
"passport_anchor": {
"tx_hash": "0x7777777777777777777777777777777777777777777777777777777777777777",
"basescan_url": "https://basescan.org/tx/0x7777777777777777777777777777777777777777777777777777777777777777",
"merkle_root": "0x2222222222222222222222222222222222222222222222222222222222222222",
"anchored_at": "2026-08-18T07:05:44.220118Z",
"passport_version": 3,
"data_hash_matches": true
}
}HTTP code 200 as well for a minimal item. An item with no certificate in force, no published passport, no media, no history and whose batch was not anchored returns the same structure with empty values.
{
"token_id": null,
"uid_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"product_name": "Sac de voyage Exemple SAS",
"brand_name": "Exemple SAS",
"category_name": null,
"sku": null,
"description": null,
"metadata_uri": "ipfs://bafyexemple00000000000000000000000000000000000000000000000",
"created_at": "2026-08-14T09:12:33.418000Z",
"tx_hash": null,
"contract_address": "0x0000000000000000000000000000000000000000",
"certificate": null,
"passport": null,
"media": [],
"events": [],
"merkle_anchor": null,
"passport_anchor": null
}#The top-level fields
| Field | Type | Description |
|---|---|---|
token_id | string or null | The identifier of the token, as a string. null as long as the mint has not been confirmed on the chain. |
uid_hash | string or null | The hash of the item, as it is recorded. |
product_name | string or null | The name of the item. |
brand_name | string or null | The name of the owning brand. null if no brand is attached. |
category_name | string or null | The name of the category. null if no category is attached. |
sku | null | Always null. The field appears in the response and is never filled in by this endpoint. |
description | null | Always null. Same remark as for sku. |
metadata_uri | string or null | The address of the metadata of the item. |
created_at | string or null | Date of creation of the record, formatted as ISO 8601 in universal time. |
tx_hash | string or null | The mint transaction. It does not change when the item is transferred, so it is the link of proof of origin. |
contract_address | string or null | The address of the contract that carries this token. |
certificate | object or null | The certificate currently valid. null if the item has none. |
passport | object or null | The published passport, filtered at the consumer tier. null if no published passport exists. |
media | array | The media of the item. Empty array if none. |
events | array | The public history of the item. Empty array if none. |
merkle_anchor | object or null | The proof of inclusion of the item in a batch anchored on Base. |
passport_anchor | object or null | The anchoring of the content of the passport. |
#certificate
The server returns the most recently issued certificate among those that are still active at the moment of the call. It discards a revoked certificate, and it discards a certificate whose expiry date has passed.
| Field | Type | Description |
|---|---|---|
certificate_number | string | The number of the certificate. It is also one of the four forms of identifier accepted by this endpoint. |
status | string | Always reads active on this endpoint. A revoked or expired certificate is not returned here, the certificate field then reads null. To read the revoked or expired state, call GET /v1/certificate/{identifier}. |
issued_at | string | Date of issuance, formatted as ISO 8601. |
expires_at | string or null | Date of end of validity. No certificate issued by the platform carries one today, the value is always null. Do not build your integration on an end date. |
issuer_name | string or null | The name of the brand that issued the certificate. The server computes this field on read. null when the certificate is attached to no brand. |
#passport
The passport returned is the one that carries the highest version number among the published versions. The server first looks for a passport attached to the model of the item, then for a passport attached to the item itself. The server returns here neither the drafts nor the passports reserved for the brand.
| Field | Type | Description |
|---|---|---|
schema_version | string | The version of the data schema of the passport. |
passport_version | integer | The version number of the passport, incremented at every publication. |
data | object | The content of the passport, filtered at the consumer tier. Its structure depends on the product category and on what the brand filled in. |
published_at | string or null | Date of publication of this version. |
data_hash | string or null | The hash of the content, 64 hexadecimal characters, with no 0x prefix. This hash goes into the leaf of the tree whose root is recorded on the chain. The passport_anchor.merkle_root field returns that root. |
The filtering keeps the sections intended for the public and for the end customer, when the passport contains them: product identity, ESPR compliance, REACH compliance, CE marking, labels, battery specification, circularity, environmental impact, durability, energy efficiency, carbon footprint, primary materials and the mention of organic certification.
The server removes everything else before sending, including inside a section
that is partly kept. In the example above, the materials section of the full
passport also describes the lining and the hardware: those two entries do not
come out through this endpoint.
A brand can define its own access rules for its product groups. Those rules then entirely replace the default split described above, including for the public tier: a public rule set on the manufacturing bill of materials makes it come out through this endpoint.
#media
Up to 20 entries, in the display order defined by the brand.
The server first takes the media attached to the item. If there are none, it
takes those of the model. As a last resort, it returns the cover image of the
model, alone, with the identifier 0. That 0 value signals an entry built
for the occasion, with no record of its own.
| Field | Type | Description |
|---|---|---|
id | integer | The identifier of the media. 0 for the last resort cover image. |
file_name | string | The name of the file. |
media_type | string | image, video, document or 3d_model. |
url | string | The public address of the file. The server removes from the list a media item whose address it cannot build. |
alt_text | string or null | The alternative text entered by the brand. |
#events
Up to 20 entries, from the most recent to the oldest. The list is drawn from the last 60 recorded events, then cleaned up.
The server removes two families of events. The buyback steps that changed nothing on the object: offer, refusal, expiry, agreement not settled. And the transfers of ownership, which do not appear in this list.
| Field | Type | Description |
|---|---|---|
id | integer | The identifier of the event. |
event_type | string | The type of event. Possible values: repair, warranty_activation, warranty_extension, resale, return, inspection, recall, end_of_life, custom, quality_control, reconditioning, distribution, after_sale_service, maintenance, certification, recycling, donation, destruction. |
description | string or null | The free text entered by the author of the event. |
occurred_at | string | Date of the event, formatted as ISO 8601. |
actor_name | string or null | The name of the event's author. It is null as soon as the recorded value contains an @. This endpoint is entirely public and accepts the serial number printed on the label: holding the object must not hand over its owner's email address, and a truncated address would be guessable. A name the actor chose does stay visible. |
#merkle_anchor
Present only if three conditions are met: the item belongs to a batch whose
root was anchored on Base, the item carries a token identifier, and the root
recomputed today is identical to the anchored root. If the batch has changed
since the anchoring, the proof would be unverifiable on the chain and the
field reads null. The server never returns a misleading proof.
| Field | Type | Description |
|---|---|---|
anchor_tx_hash | string | The transaction that recorded the root on Base. |
root | string | The anchored root. |
leaf_index | integer | The position of the leaf of this item, counted from 0. |
leaf | string | The hash of the leaf of this item. |
proof | array of string | The sibling hashes, from the bottom up, that make it possible to recompute the root from the leaf. |
You can verify this proof yourself, without trusting us. The recomputation
starts from leaf, applies the entries of proof in order, and must end at
root.
The hash used is keccak256. The pairing convention is that of OpenZeppelin: at
each level, you concatenate the 32 bytes of the current value and the 32 bytes
of the proof entry in ascending order, then you apply keccak256 to the
result.
from eth_utils import keccak # pip install eth-utils
root = "0x2222222222222222222222222222222222222222222222222222222222222222"
leaf = "0x3333333333333333333333333333333333333333333333333333333333333333"
proof = [
"0x5555555555555555555555555555555555555555555555555555555555555555",
"0x6666666666666666666666666666666666666666666666666666666666666666",
]
node = bytes.fromhex(leaf[2:])
for entree in proof:
voisin = bytes.fromhex(entree[2:])
node = keccak(node + voisin) if node < voisin else keccak(voisin + node)
print("0x" + node.hex() == root)The values above are made up, so this program prints False. Replace them
with those of a real response and it prints True.
#passport_anchor
Present only if this exact version of the passport was anchored and if the
anchoring transaction exists. This field dates the content of the passport.
The merkle_anchor field above dates the item. The two stay separate because
they do not prove the same thing.
| Field | Type | Description |
|---|---|---|
tx_hash | string | The transaction that recorded the root on Base. |
basescan_url | string or null | The direct link to this transaction on the explorer of the chain. |
merkle_root | string or null | The anchored root. |
anchored_at | string or null | Date of the anchoring, formatted as ISO 8601. |
passport_version | integer or null | The version of the passport covered by this anchoring. |
data_hash_matches | boolean or null | true when the content stored today matches what was anchored. false signals that the passport has changed since. |
No proof of inclusion accompanies the anchoring of the passport. To verify the
inclusion yourself, call GET /v1/passport/{identifier}/proof, which returns
the leaf, its position and the sibling hashes.
#Errors
| Code | Condition | What to do |
|---|---|---|
| 404 | No item in the catalog matches this identifier, in any of the four forms. Message Product not found. A destroyed, replaced or archived item gives the same response. | Check the value sent. If the item was destroyed, replaced or withdrawn from the catalog, this code is final. |
| 404 | No identifier was provided, the call stops at /v1/resolve. Message Not Found. | Add the identifier in the path. |
| 405 | A method other than GET was sent on this path. Message Method Not Allowed. The response carries the header Allow: GET. | This endpoint answers only in GET. |
| 429 | More than 60 calls in 60 seconds from the same IP address. Message Rate limit exceeded: 60 requests per 60s. | Wait the number of seconds given by Retry-After. Spread your calls over time. |
| 500 | Unexpected server error. Fixed body {"detail": "Internal Server Error"}. | Retry. The X-Request-Id header identifies the call, pass it on to us if it repeats. |
A value that resembles none of the four expected forms receives a 404.
#See also
GET /products/{uid}/public, read the public information of a product.GET /timeline/{identifier}, read the public history of a product.GET /certificate/{identifier}, read the certificate of authenticity of an item.GET /passport/{identifier}/proof, gather the public proofs of the passport of an item.
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.