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.

#Authorization

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

600 calls per 60 second window, counted per calling IP address. The limit is wide because hundreds of visitors at a trade fair or on a mobile network often share one address. It 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.

A successful response carries the header Cache-Control: public, max-age=30, s-maxage=120: it reads no session and stays the same for every caller. An error carries no-store, max-age=0. 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

NameTypeRequiredDescription
identifierstringyesThe identifier of the item. Four accepted forms, described below. The server strips the edge spaces before the search.
langstringnoThe language of the category name: fr or en. Failing that, the server reads the Accept-Language header, then falls back to en.

A single query parameter, lang, and it is optional. It changes one thing in the response: the language of category_name. Everything else is identical whatever its value.

The server picks the language in this order: the lang parameter when it is fr or en, otherwise the first recognised tag of the Accept-Language header, otherwise en. A value the server does not recognise is ignored without an error: you get en, and not a 422.

#The four forms of identifier

The server tries them in this order and stops at the first one that finds an item.

OrderFormRecognized byCase sensitive
1Hash of the item0x followed by 64 hexadecimal characters, that is 66 charactersno
2Identifier of the tokenAny value, compared as it stands to the recorded token identifieryes
3Printed serial number12 characters of the Crockford Base32 alphabet, which excludes the letters I, L, O and Uno
4Certificate numberThe exact value of the certificate_number field, for example ST-CERT-000000000000yes

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/0x0000000000000000000000000000000000000000000000000000000000000000

#Example response

HTTP code 200. A complete item, with a certificate, a published passport, one media item, one event and both proofs of anchoring.

JSON
{
  "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": "03701234567891",
        "model": "Sac de voyage",
        "brand": "Exemple SAS",
        "made_in": "FR",
        "production_facility": "Atelier Exemple SAS"
      },
      "circularity": {
        "recyclability_percentage": 62,
        "recycled_content_percentage": 0
      },
      "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/example-sas/sac-face.jpg",
      "alt_text": "Sac de voyage vu de face"
    }
  ],
  "events": [
    {
      "id": 9137,
      "event_type": "warranty_activation",
      "description": "Warranty activated at in-store purchase.",
      "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",
    "confirmed_at": null,
    "block_timestamp": "2026-08-18T07:05:58.000000Z",
    "block_number": 34129877,
    "passport_version": 3,
    "data_hash_matches": true
  },
  "evidence": {
    "sections": {
      "identity": "verified",
      "integrity": "verified",
      "composition": "declared"
    },
    "legend": {
      "declared": "Stated by the brand. Recorded, dated and attributable, but not independently checked.",
      "verified": "Rests on an on-chain reference that anyone can re-check independently. SealTrust records it and does not re-query the chain on each request."
    },
    "derived": true,
    "note": "Statuses are computed by SealTrust from verifiable facts. A brand cannot set them."
  }
}

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.

JSON
{
  "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,
  "unit_anchor": null,
  "evidence": null
}

#The top-level fields

FieldTypeDescription
token_idstring or nullThe identifier of the token, as a string. null as long as the mint has not been confirmed on the chain.
uid_hashstring or nullThe hash of the item, as it is recorded.
product_namestring or nullThe name of the item.
brand_namestring or nullThe name of the owning brand. null if no brand is attached.
brand_hide_powered_bybooleantrue when the "Powered by SealTrust" wording must disappear. Two conditions, and both are required: the brand's plan includes white labeling and the brand has chosen to hide the wording in its console. By default the wording stays, including for a brand that has just subscribed to Inside and changed nothing.
brand_public_hoststring or nullThe domain name under which the brand publishes its public pages, for example verify.example-sas.test. Populated only when the brand has white labeling and that domain has been verified, or, for a reseller's client brand with no domain of its own, when that reseller's shared domain is verified. Otherwise null. A page uses it to declare its canonical address and its sharing previews under the brand's domain rather than ours.
brand_served_hostsarray of stringEvery verified domain name the brand's pages are served on: its own and its reseller's shared one. brand_public_host is only the preferred one. A page uses it to accept both, so that a label already printed on the reseller's domain keeps opening its page once the brand adopts a domain of its own. Empty list when the brand is served only on ours.
category_namestring or nullThe name of the category, in the caller's language: see the lang parameter above. null if no category is attached.
skunullAlways null. The field appears in the response and is never filled in by this endpoint.
descriptionnullAlways null. Same remark as for sku.
metadata_uristring or nullThe address of the metadata of the item.
created_atstring or nullDate of creation of the record, formatted as ISO 8601 in universal time.
tx_hashstring or nullThe mint transaction. It does not change when the item is transferred, so it is the link of proof of origin.
contract_addressstring or nullThe address of the contract that carries this token.
certificateobject or nullThe certificate currently valid. null if the item has none.
passportobject or nullThe published passport, filtered at the consumer tier. null if no published passport exists.
mediaarrayThe media of the item. Empty array if none.
eventsarrayThe public history of the item. Empty array if none.
merkle_anchorobject or nullThe proof of inclusion of the item in a batch anchored on Base.
passport_anchorobject or nullThe anchoring of the content of the passport.
unit_anchorobject or nullFor a piece registered by its lot (anchored mode): the lot's root recorded on Base and the piece's proof. null for any other piece.
evidenceobject or nullOn what basis each section can be believed. See below. null when no published passport is served.

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

FieldTypeDescription
certificate_numberstringThe number of the certificate. It is also one of the four forms of identifier accepted by this endpoint.
statusstringAlways 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_atstringDate of issuance, formatted as ISO 8601.
expires_atstring or nullDate 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_namestring or nullThe 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 item itself, then for the one of its production batch, and falls back on the model's reference passport only when it finds none. The order matters: an item that has its own passport must never receive another unit's, along with that other unit's serial number and battery state. The server returns here neither the drafts nor the passports reserved for the brand.

FieldTypeDescription
schema_versionstringThe version of the data schema of the passport.
passport_versionintegerThe version number of the passport, incremented at every publication.
dataobjectThe content of the passport, filtered at the public tier. Its structure depends on the product category and on what the brand filled in.
published_atstring or nullDate of publication of this version.
data_hashstring or nullThe 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, when the passport contains them: product identity, manufacturer identification, ESPR compliance, REACH compliance, CE marking, the Article 13(4) and 13(5) markings, the link to the EU declaration of conformity, labels, battery specification, ingredients, the name and address of a cosmetic's responsible person, and within circularity the recyclability rate, the recycled content, the extinguishing agent and the waste management link.

Environmental impact, the rest of circularity, durability, energy efficiency, carbon footprint, primary materials, care and movement belong to the end_user tier: they do not come out through this endpoint. A signed-in customer reads them on GET /passport/{identifier}.

A separate section, "EU declaration of conformity", carries the content of the declaration itself once the brand has filled it in: number, date and place of issue, the legal person under whose responsibility it is issued, its object, the Union legislation covered, the harmonised standards applied, and where relevant the notified body with its identification number and its certificate. Fields left empty are not published, and the whole section is absent until something is filled in: an empty block would assert that a document exists without saying anything about it.

For a battery passport, this split follows Annex XIII of regulation (EU) 2023/1542: point 1 adds the hazardous substances, the critical raw materials, the carbon footprint and its class, the due diligence report, the post-consumer recycled content per metal and the share of renewable content. The other way round, three fields come out, because points 2 and 4 reserve them to persons with a legitimate interest: the part numbers, the battery status and the expected lifetime in cycles.

For a textile passport, the name and the percentage of each material are public: this is the fibre composition that Regulation (EU) No 1007/2011 requires on the label and wants visible before the purchase, online included. The origin of the materials stays restricted. For a footwear passport, the material of the upper, the lining, the insole and the sole is public, as on the label Directive 94/11/EC puts on the shoe. These label mentions, like the public data of Annex XIII, stay public whatever the brand's rules.

The fields the console writes at the root of the passport (composition, care_instructions, size...) each have their own tier, described in Passport access rules. Those that identify the product, the fibre composition, the materials of a shoe, the size, the colour and the weight are public. Care instructions belong to the end_user tier.

The server removes everything else before sending, including inside a section that is partly kept. In the example above, the circularity section of the full passport also carries the repairability index and the expected lifetime: 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.

FieldTypeDescription
idintegerThe identifier of the media. 0 for the last resort cover image.
file_namestringThe name of the file.
media_typestringimage, video, document or 3d_model.
urlstringThe public address of the file. The server removes from the list a media item whose address it cannot build. For an image, it is its light copy (WebP, 800 pixels at most on the long side, 100 KB at most, no EXIF data) when it exists, else the original.
alt_textstring or nullThe 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.

FieldTypeDescription
idintegerThe identifier of the event.
event_typestringThe 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.
descriptionstring or nullThe free text entered by the author of the event.
occurred_atstringDate of the event, formatted as ISO 8601.
actor_namestring or nullThe 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.

FieldTypeDescription
anchor_tx_hashstringThe transaction that recorded the root on Base.
rootstringThe anchored root.
leaf_indexintegerThe position of the leaf of this item, counted from 0.
leafstringThe hash of the leaf of this item.
proofarray of stringThe 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.

Recomputing the root
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.

FieldTypeDescription
tx_hashstringThe transaction that recorded the root on Base.
basescan_urlstring or nullThe direct link to this transaction on the explorer of the chain.
merkle_rootstring or nullThe anchored root.
block_timestampstring or nullThe date of the inscription, ISO 8601. This is when the chain sealed the block, and the only date that counts.
confirmed_atnullAlways null on this route. This field says when we read the transaction receipt, not when the chain sealed: on an anchor verified after the fact, it is the day of the check. It carries nothing for someone verifying a product, so this route does not serve it.
block_numberinteger or nullThe block holding the transaction.
anchored_atstring or nullThe date the transaction was SENT, ISO 8601. It precedes the inscription and does not prove it. The date that counts is the timestamp of the block, served above in block_timestamp.
passport_versioninteger or nullThe version of the passport covered by this anchoring.
data_hash_matchesboolean or nulltrue 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.

#unit_anchor

Present only for a piece created in anchored mode: its lot is recorded on Base under one single root, and the piece has its own token only from its first claim (pieces registered by lot). The block is the same as the response of GET /v1/verify/unit-anchor/{identifier}, without the read of the contract: onchain_root_matches is always null here.

FieldTypeDescription
statusstringrecorded when the transaction is confirmed and succeeded, pending before, failed if the chain refused it.
leaf_fieldsobjectThe recorded fields: serial, brand_code, product_model_id, lot_code, uid_hash, created_at_unix.
leafstringThe fingerprint of this piece's leaf.
leaf_indexintegerThe position of the leaf, counted from 0.
proofarray of stringThe sibling fingerprints, from the leaf up to the root.
proof_validbooleanThe leaf recomputed from leaf_fields and climbed with proof gives merkle_root.
merkle_rootstringThe root of the lot.
onchain_batch_idintegerThe identifier under which the contract keeps this root.
tx_hashstring or nullThe transaction that recorded the root. null until it has been sent.
explorer_urlstring or nullThe link to this transaction on the chain's explorer.
recorded_atstring or nullThe date of the record, the block's timestamp. null until status is recorded.
token_mintedbooleantrue when the piece already has its own token.

#evidence

On what basis each section of the passport can be believed. It is the same block, computed by the same function, as the one on GET /v1/passport/{identifier}.

Three values exist, and they are computed by SealTrust. A brand cannot choose them.

ValueWhat it says
verifiedRests on an on-chain reference that anyone can re-check independently. SealTrust records it and does not re-query the chain on each request.
document_backedA third-party document is attached and can be retrieved. Its content has not been audited by SealTrust.
declaredDeclared by the brand. Recorded, dated, attributable, not independently verified.

Four sections can carry one of these values: identity, integrity, composition and substances_of_concern. Only the sections the passport actually contains receive one.

identity and integrity are always present, because they bear on the unit itself. The identity section goes to verified when the unit carries a token identifier on the chain. The integrity section goes to verified when this version's hash has been anchored and still matches the recorded data.

composition and substances_of_concern appear only when the passport data carries them. A materials block left empty (rows with no name, no share and no origin) describes no material. In the example response above, the full passport describes its materials, absent from the public block, and declares no substance of concern, so sections holds three entries. The number of entries ranges from two to four depending on the passport served.

The block additionally carries legend, which restates the meaning of the values present, derived at true, and note.

No other section of the passport receives a status. A section that sections does not name is neither verified nor disproved, it is not rated. So display a status only for the sections this field names.

The field reads null when no published passport is served, and null if its computation fails. A client that does not find the block displays no status.

#Errors

CodeConditionWhat to do
404No 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.
404No identifier was provided, the call stops at /v1/resolve. Message Not Found.Add the identifier in the path.
404The call arrives on a brand's verified domain name, and the product belongs to another brand. detail is Product not found.Call this address on the domain of the brand concerned, or on api.sealtrust.io. The message is the same as for an unknown identifier, so as not to confirm that it exists.
405A 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.
429More than 600 calls in 60 seconds from the same IP address. Message Rate limit exceeded: 600 requests per 60s.Wait the number of seconds given by Retry-After. Spread your calls over time.
500Unexpected 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

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