Method GET/passport /{identifier} /proof
Gather in one call every public proof of a passport: content hash, IPFS copy, anchor on Base, version seal and state of the signed credential. Public endpoint.
On this page
You gather in a single call the public proofs attached to a product's passport. By the end of this page, you will know how to ask for these proofs from an item identifier, read what each block actually establishes, and tell a missing proof from a failing proof.
Full address:
GET https://api.sealtrust.io/v1/passport/{identifier}/proofThe same endpoint also answers without the /v1 prefix, at
https://api.sealtrust.io/passport/{identifier}/proof. Both addresses call
the same code. Use the /v1 form for a new integration.
This endpoint returns no passport content. It returns the elements that let
you verify that content. To read the passport itself, call
GET /v1/passport/{identifier}.
#
None, this is a public endpoint. You send no API key, no session, and no origin header. The response is the same for everyone.
You receive here only the passports whose visibility is public. A passport reserved for the owner or reserved for the brand is never returned to you by this endpoint.
#Rate limit
60 calls per 60 second slice, counted per calling network address.
This counter is shared by every path that starts with /passport. The calls
you make to one of them therefore eat into the budget of the others. The /v1
prefix does not create a second budget: /v1/passport/1042/proof and
/passport/1042/proof fill the same counter.
Every accepted response carries three headers.
| Header | Content |
|---|---|
X-RateLimit-Limit | the limit applied over the window, here 60 |
X-RateLimit-Remaining | what you have left in the current window |
X-RateLimit-Reset | the timestamp of the end of the window, in seconds |
A refusal returns 429, with these three headers and Retry-After. On this
endpoint, Retry-After equals the duration of the window, that is 60 seconds.
#Path and query parameters
| Name | Type | Required | Description |
|---|---|---|---|
identifier | string | yes | The item whose proofs you want. Three forms are accepted, see below. |
This endpoint has no query parameter.
identifier accepts three forms, tried in this order.
| Form | Appearance | Where it comes from |
|---|---|---|
| Identifier hash | 0x followed by 64 hexadecimal characters | the hash of the unique identifier of the unit. On an NFC item it comes from the identifier of the chip. On a QR-only item, the server draws it at random at mint time. |
| Token identifier | a number written in decimal | the identifier of the item on the chain |
| Printed serial number | 12 characters | what the QR code on the product carries, in the address /p/{serial} |
You can write the serial number in uppercase or in lowercase. We fold the
characters that look alike into a single form before the search, so an I or
an L typed by hand finds the 1, and an O finds the 0.
This endpoint resolves only the items still in the brand's catalog. An item destroyed on the chain, replaced by a later version, or archived answers 404.
#Headers
No header is required.
#Request body
None. This request has no body.
#Example request
Proofs of the passport of the item whose token identifier is 1042.
curl -i https://api.sealtrust.io/v1/passport/1042/proofconst reponse = await fetch(
"https://api.sealtrust.io/v1/passport/1042/proof",
);
if (reponse.status === 404) {
console.log("Aucun passeport public pour cet identifiant.");
} else if (reponse.ok) {
const preuves = await reponse.json();
console.log(preuves.passport_version, preuves.data_hash);
if (preuves.anchor && preuves.anchor.anchored === true) {
console.log("Lot ancré sur", preuves.anchor.chain);
console.log(preuves.anchor.merkle_root);
} else if (preuves.anchor) {
console.log("Pas d'ancrage de lot. Référence :", preuves.anchor.proves);
}
if (preuves.passport_anchor) {
console.log(
"Document daté au plus tard le",
preuves.passport_anchor.anchored_at,
);
console.log(
"Contenu inchangé :",
preuves.passport_anchor.data_hash_matches,
);
}
console.log("Scellé :", preuves.seal.sealed);
console.log("Justificatif signé :", preuves.vc.issued);
} else {
console.log(reponse.status, await reponse.json());
}import requests
response = requests.get(
"https://api.sealtrust.io/v1/passport/1042/proof",
timeout=30,
)
if response.status_code == 404:
print("Aucun passeport public pour cet identifiant.")
elif response.ok:
preuves = response.json()
print(preuves["passport_version"], preuves.get("data_hash"))
ancrage = preuves.get("anchor")
if ancrage and ancrage.get("anchored") is True:
print("Lot ancré sur", ancrage["chain"])
print(ancrage.get("merkle_root"))
elif ancrage:
print("Pas d'ancrage de lot. Référence :", ancrage.get("proves"))
document = preuves.get("passport_anchor")
if document:
print("Document daté au plus tard le", document["anchored_at"])
print("Contenu inchangé :", document["data_hash_matches"])
print("Scellé :", preuves["seal"]["sealed"])
print("Justificatif signé :", preuves["vc"]["issued"])
else:
print(response.status_code, response.json())#Example response
HTTP code 200.
{
"passport_version": 3,
"data_hash": "1111111111111111111111111111111111111111111111111111111111111111",
"ipfs_uri": "ipfs://bafybeiexemple00000000000000000000000000000000000000000000",
"ipfs_gateway_url": "https://ipfs.io/ipfs/bafybeiexemple00000000000000000000000000000000000000000000",
"anchor": {
"chain": "base",
"chain_id": 8453,
"type": "merkle_batch",
"tx_hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
"basescan_url": "https://basescan.org/tx/0x2222222222222222222222222222222222222222222222222222222222222222",
"merkle_root": "0x3333333333333333333333333333333333333333333333333333333333333333",
"anchored": true,
"proves": "batch_inclusion"
},
"passport_anchor": {
"chain": "base",
"chain_id": 8453,
"tx_hash": "0x4444444444444444444444444444444444444444444444444444444444444444",
"basescan_url": "https://basescan.org/tx/0x4444444444444444444444444444444444444444444444444444444444444444",
"merkle_root": "0x5555555555555555555555555555555555555555555555555555555555555555",
"leaf": "0x6666666666666666666666666666666666666666666666666666666666666666",
"leaf_index": 2,
"proof": [
"0x7777777777777777777777777777777777777777777777777777777777777777",
"0x8888888888888888888888888888888888888888888888888888888888888888"
],
"anchored_at": "2026-08-14T02:15:09+00:00",
"data_hash_matches": true,
"proves": "content_existed_at_or_before_tx"
},
"seal": {
"sealed": true,
"sealed_at": "2026-08-13T09:41:22+00:00",
"algorithm": "st-dpp-chain-v1",
"version_hash": "9999999999999999999999999999999999999999999999999999999999999999",
"prev_version_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"linked": true,
"chain_link_match": true
},
"vc": {
"issued": true,
"vct": "https://schema.sealtrust.io/vct/digital-product-passport",
"issued_at": "2026-08-13T09:41:25+00:00"
},
"verifications": {
"count": 47,
"last_verified_at": "2026-08-19T18:03:44+00:00"
}
}passport_version, seal and vc are always present. We omit the other
top-level fields when the matching proof does not exist. An absence means that
this proof is not available. Treat it as information, in the same way as a
value.
| Field | Type | Presence | Description |
|---|---|---|---|
passport_version | integer | always | The version number of the published passport that every proof below relates to. |
data_hash | string | if recorded | The SHA-256 hash of the full content of the passport, 64 hexadecimal characters with no 0x prefix. |
ipfs_uri | string | if the copy is verified | The IPFS address of the public copy of the passport. |
ipfs_gateway_url | string | if the copy is verified | The same copy, as a web address you can open in a browser. |
anchor | object | if the item has a reference on the chain | The reference on the chain that concerns the ITEM. See below. |
passport_anchor | object | if the version was anchored | The anchoring that dates the DOCUMENT. See below. |
seal | object | always | The seal of the version and its place in the sequence of versions. See below. |
vc | object | always | The state of the signed credential attached to the passport. See below. |
verifications | object | if the count is above zero | The number of successful verifications recorded for this unit, by QR as well as by NFC, and the date of the last one. Also absent when the unit carries no identifier hash. |
The passport served here covers either this precise unit or its model. We
first look for a passport attached to the unit. Failing that, we serve the
passport published for its model, shared by every unit that carries the same
product code. In that second case, data_hash, ipfs_uri,
ipfs_gateway_url, passport_anchor, seal and vc describe the document
of the model. anchor and verifications remain specific to the unit you
asked for.
The response also 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.
#The anchor block, the reference of the item
This block is present when the item carries a reference on the chain. It
covers two different situations, and it is the anchored field that separates
them.
| Field | Type | Description |
|---|---|---|
chain | string | The network, in machine readable form. base in production. |
chain_id | integer | The identifier of the network. 8453 for Base in production. |
type | string | merkle_batch or mint_transaction. |
tx_hash | string | The transaction concerned. |
basescan_url | string | The direct link to this transaction on the public explorer of the network. |
merkle_root | string | The batch root recorded on the chain. Present only when type reads merkle_batch and the root is recorded. |
anchored | boolean | true only when the batch of the item was recorded on the chain. |
proves | string | batch_inclusion when the batch is anchored, token_minted otherwise. |
anchored: true means that the batch the item belongs to was recorded on the
chain. You obtain the inclusion proof itself from
GET /v1/verify/merkle/{identifier}, when this block also carries
merkle_root. Without merkle_root, that call answers 404 and detail reads
No Merkle anchor for this product.
anchored: false means that the batch of the item was never recorded. The
block then carries the mint transaction of the item. It shows that the token
exists on the chain. It shows nothing about a batch, and nothing about the
content of the passport.
In both cases, this reference carries no commitment on the content of the
passport. The commitment on the content is read in passport_anchor and in
seal.
#The passport_anchor block, the date of the document
This block is present when this precise version of the passport was recorded on the chain. It establishes one single thing: the content existed at the latest at the moment of that transaction.
| Field | Type | Description |
|---|---|---|
type | string | Always passport_anchor. It tells this anchoring apart from the one in the anchor block, which carries a batch of items. |
anchored | boolean | Always true. The block exists only once the version has been recorded on the chain. |
chain | string | The network, in machine readable form. |
chain_id | integer or null | The identifier of the network. null on an anchoring that predates the recording of the network. chain then indicates the production network. |
tx_hash | string | The transaction that carries the root. |
basescan_url | string | The direct link to this transaction on the public explorer of the network. |
merkle_root | string | The root recorded on the chain, 0x followed by 64 hexadecimal characters. |
leaf | string | The hash of this version in the tree, 0x followed by 64 hexadecimal characters. |
leaf_index | integer | The position of this hash in the list of the recorded hashes. Counting starts at 0. |
proof | string[] | The neighboring hashes to combine with leaf, from the bottom up, to recover merkle_root. The list is empty when the recording covered only one version. |
anchored_at | string or null | The date of the recording, formatted as ISO 8601. |
data_hash_matches | boolean | true when the passport recorded today still matches what was recorded on the chain. |
proves | string | Always content_existed_at_or_before_tx. |
You can redo the inclusion check without trusting us. You start from leaf.
For each element of proof, in order, you place the two 32 byte values side
by side, the smaller of the two first, then you apply keccak256 to the
concatenation. The result becomes the new working value. After the last
element of proof, you must obtain exactly merkle_root. This is the
verification convention of OpenZeppelin, with neighbors sorted at each level.
Then open basescan_url to read the transaction that recorded this root.
The absence of passport_anchor is common and normal. Publishing a version
does not record it on the chain. The recording is a separate operation, done
in grouped passes, and many published passports never carry this block. So
conclude nothing from its absence about the validity of the passport. The
seal block, on the other hand, is always present and covers the versions
that were not recorded.
#The seal block, the seal of the version
This block is always present. It holds on the versions that
passport_anchor does not cover.
| Field | Type | Presence | Description |
|---|---|---|---|
sealed | boolean | always | false when the version was never sealed. The block then stops there. |
sealed_at | string | if sealed | The date of the sealing, formatted as ISO 8601. |
algorithm | string | if sealed | The version of the chaining algorithm. Reads st-dpp-chain-v1. |
version_hash | string or null | if sealed | The hash of this link, 64 hexadecimal characters. |
prev_version_hash | string or null | if sealed | The hash of the previous link. null for the first published version. |
linked | boolean | if sealed | true when this version carries a link hash. |
reason | string | if linked is false | Reads sealed_before_chain. The version was published before chaining existed, so no link is manufactured after the fact. The block stops there. |
chain_link_match | boolean | if linked | true when the passport recorded today still produces exactly the stored link. |
chain_link_match is recomputed on every call from the recorded content.
false means that the version was changed after it was published.
#The vc block, the signed credential
This block is always present.
| Field | Type | Presence | Description |
|---|---|---|---|
issued | boolean | always | true when a signed credential was issued for this passport. |
vct | string | if recorded | The identifier of the credential template. Today it reads https://schema.sealtrust.io/vct/digital-product-passport. |
issued_at | string | if recorded | The date of issuance, formatted as ISO 8601. |
#The IPFS copy
ipfs_uri and ipfs_gateway_url designate the public copy of the passport.
The content at that address is the public view of the passport. Its hash
therefore differs from data_hash, which covers the full content.
You receive these two fields only when we have fetched the copy and checked that it is exactly the public passport that the brand publishes today. Their absence can therefore mean three things: no copy was ever deposited, the copy could not be fetched at the moment of the call, or its content no longer matches what the brand publishes. A brand that changes its access rules changes what its public passport shows, so a copy deposited before that change stops being announced until a new one has been deposited. Conclude nothing from their absence alone.
The host of ipfs_gateway_url is that of a public gateway and can change. Do
not hard code it. Use the address returned as it stands, or ipfs_uri with
the gateway of your choice.
#Errors
The body of an error response carries a detail field.
| Code | Condition | What to do |
|---|---|---|
| 404 | No item in the catalog matches this identifier, under any of the three accepted forms. detail reads Product not found. | Check the identifier. An item destroyed on the chain, replaced by a later version, or archived no longer resolves and gives this same response. |
| 404 | The item exists, but no passport published with public visibility is attached to it, neither directly nor through its model. detail reads No published passport found for this product. | Do not treat this response as a failure. This item has no public passport. A passport reserved for the owner or for the brand gives the same response. |
| 429 | The limit of 60 calls per 60 seconds is reached for your network address, across all the /passport paths. detail reads Rate limit exceeded: 60 requests per 60s. | Wait the number of seconds given by Retry-After, then retry. Cache the response on your side. |
| 500 | An unexpected error occurred while processing your call. detail reads Internal Server Error. The response carries an X-Request-Id header. | Retry. If the error persists, contact support with the value of X-Request-Id. |
An anchoring read or an IPFS copy read that fails does not produce an error. The block concerned is absent, and the other proofs are served normally.
#See also
GET /passport/{identifier}/verify, check the integrity of the published passport of an item.GET /verify/merkle/{identifier}, retrieve the proof of inclusion of an item in the batch anchored on Base.GET /passport/{identifier}, read the published passport of an item.- Trust and proofs, what each proof establishes and how a third party redoes the verification.
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.