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

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

NameTypeRequiredDescription
identifierstringyesThe 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.

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": "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.

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
}

#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.
category_namestring or nullThe name of the category. 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.

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

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

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.
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.
anchored_atstring or nullDate of the anchoring, formatted as ISO 8601.
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.

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