Method GET/passport /{identifier} /verify
Check the integrity of the published passport of an item: recorded hash, immutable public copy and version seal. Public endpoint.
On this page
You check that a published passport has not been modified since its publication. By the end of this page, you will know how to ask for this check from any item identifier, how to read the three verdicts the response returns, and how to tell an altered passport from a check that could not complete.
Full address:
GET https://api.sealtrust.io/v1/passport/{identifier}/verifyThe same endpoint also answers without the /v1 prefix, at
https://api.sealtrust.io/passport/{identifier}/verify. The two addresses
call the same code. Use the /v1 form for a new integration.
#
None, this is a public endpoint. You send no API key, no session and no origin header. The response is the same for everyone.
#Rate limit
60 calls per 60-second window, counted by calling network address.
This counter is shared by every path that starts with /passport. The
calls you send to one of them therefore eat into the budget of the others. The
/v1 prefix does not create a second budget:
/v1/passport/0ABCDEFGHJKM/verify and /passport/0ABCDEFGHJKM/verify
fill the same counter.
Plan for a timeout of at least 30 seconds on the client side. During your
call, this endpoint goes to fetch the immutable public copy from public
gateways. It queries several of them one after the other, each with its own time
limit, so a first call on a copy the server has never read yet can last some
twenty seconds. The calls that follow on the same copy answer without going to fetch
it again. When no gateway answers, the response stays 200 and ipfs_match is null.
Every accepted response carries three headers.
| Header | Content |
|---|---|
X-RateLimit-Limit | the cap applied over the window, here 60 |
X-RateLimit-Remaining | what is left to you in the current window |
X-RateLimit-Reset | the end-of-window timestamp, in seconds |
A refusal returns 429, with these three headers and Retry-After. On this
endpoint, Retry-After is the duration of the window, that is 60 seconds.
#Path and query parameters
| Name | Type | Required | Description |
|---|---|---|---|
identifier | string | yes | The item whose passport you want to check. Three forms are accepted, see below. |
This endpoint has no query parameter.
identifier accepts three forms, tried in this order.
| Form | What it looks like | Where it comes from |
|---|---|---|
| Item hash | 0x followed by 64 hexadecimal characters | For an NFC item, the hash of the identifier read on the chip. For a QR item, a hash the server draws at random at the moment of minting. Both have the same form and you use them the same way. |
| Token identifier | a 256-bit integer written in decimal, 77 or 78 digits | The identifier of the item on the chain. Treat it as a string of characters: it exceeds a 64-bit integer. |
| Printed serial number | 12 characters | What the QR code carries on the product, in the /p/{serial} address. |
The server recognizes the item hash without distinction of case. It
recognizes the serial number the same way, and it brings the characters that
look alike back to a single form before searching: an I or an L that you
type by hand finds the 1 again, an O finds the 0.
This endpoint does not accept the certificate of authenticity number. You
use it on GET /certificate/{identifier}.
#Headers
No header is required.
#Request body
None. This request has no body.
#Example request
Integrity check of the passport of the item whose printed serial number
is 0ABCDEFGHJKM. The examples use this form because it fits in
12 characters. A token identifier is written at the same place in the address,
on 77 or 78 digits. The three examples set the same timeout of 30
seconds, for the reason given above.
curl -i --max-time 30 https://api.sealtrust.io/v1/passport/0ABCDEFGHJKM/verifyconst reponse = await fetch(
"https://api.sealtrust.io/v1/passport/0ABCDEFGHJKM/verify",
{ signal: AbortSignal.timeout(30000) },
);
if (reponse.status === 404) {
console.log("Aucun passeport public à contrôler pour cet article.");
} else if (reponse.ok) {
const controle = await reponse.json();
console.log(controle.db_hash_match);
console.log(controle.ipfs_match, controle.ipfs_uri);
console.log(controle.seal.sealed, controle.seal.chain_link_match);
} else {
console.log(reponse.status, await reponse.json());
}import requests
response = requests.get(
"https://api.sealtrust.io/v1/passport/0ABCDEFGHJKM/verify",
timeout=30,
)
if response.status_code == 404:
print("Aucun passeport public à contrôler pour cet article.")
elif response.ok:
controle = response.json()
print(controle["db_hash_match"])
print(controle["ipfs_match"], controle["ipfs_uri"])
print(controle["seal"]["sealed"], controle["seal"]["chain_link_match"])
else:
print(response.status_code, response.json())#Example response
HTTP code 200.
{
"db_hash_match": true,
"ipfs_match": true,
"ipfs_uri": "ipfs://bafybeiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"ipfs_gateway_url": "https://passerelle.exemple.invalid/ipfs/bafybeiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"data_hash": "1111111111111111111111111111111111111111111111111111111111111111",
"computed_hash": "1111111111111111111111111111111111111111111111111111111111111111",
"passport_version": 3,
"seal": {
"sealed": true,
"sealed_at": "2026-05-14T09:12:44+00:00",
"algorithm": "st-dpp-chain-v1",
"version_hash": "2222222222222222222222222222222222222222222222222222222222222222",
"prev_version_hash": "3333333333333333333333333333333333333333333333333333333333333333",
"linked": true,
"chain_link_match": true
}
}The response has eight fields and nothing else.
| Field | Type | Description |
|---|---|---|
db_hash_match | boolean or null | true when the recorded data of the passport still matches the hash recorded with it. null when no hash was recorded for this version. |
ipfs_match | boolean or null | true when the immutable public copy matches the public version of the passport. false when it differs from it. null when no copy exists, or when no gateway answered. |
ipfs_uri | string or null | The ipfs:// address of the immutable public copy. Filled in only when that copy is the public passport the brand publishes today, see below. |
ipfs_gateway_url | string or null | The same copy, as an HTTP address that opens in a browser. Filled in under the same conditions as ipfs_uri. |
data_hash | string or null | The hash recorded with this version of the passport, 64 hexadecimal characters. null when no hash was recorded. |
computed_hash | string | The hash recomputed at the moment of your call from the recorded data, 64 hexadecimal characters. It covers the complete data of the passport, including the fields this endpoint does not return and that the public version of the passport does not return either. You therefore cannot reproduce it yourself from public data. |
passport_version | integer | The version number of the passport checked. It starts at 1 and increases by one at every new version of the passport. |
seal | object | The seal of this version and its place in the chain of versions, see below. |
#Which passport the server checks
The check covers the latest published version whose visibility is public. The server first looks for the passport specific to the item. Failing that, it checks the passport of the model, shared by all the items of the model.
The response does not say which of the two scopes answered. Both number
their versions separately, so passport_version can be 1 in either
case.
The server never checks here a passport reserved to the owner or a passport reserved to the brand. The response is then 404, as if no passport were published.
#When the server returns the ipfs_uri address to you
You receive the address of the public copy when its content is exactly
the public passport the brand publishes today. Otherwise, ipfs_uri and
ipfs_gateway_url are null.
A brand that changes its access rules changes what its public passport
shows. A copy posted before that change is therefore no longer announced until
the brand has posted a new one. Never read these two null values
as a verdict on the copy.
ipfs_match at null says nothing about the integrity of the copy. Try again
later before concluding.
#The seal block
| Field | Type | Description |
|---|---|---|
sealed | boolean | false when the version is not sealed. The block then stops there and carries no other field. |
sealed_at | string | The date and time of the sealing, in ISO 8601 format. |
algorithm | string | The version of the chaining algorithm. Today it is st-dpp-chain-v1. |
version_hash | string or null | The sealed hash of this version, 64 hexadecimal characters. |
prev_version_hash | string or null | The sealed hash of the previous version of the same passport. null for the very first version. |
linked | boolean | true when this version carries a sealed hash and therefore takes its place in the chain. |
reason | string | Present only when linked is false. It is then sealed_before_chain: you published this version before the chaining existed, and the server manufactures none after the fact. |
chain_link_match | boolean | Present only when linked is true. false means that the recorded data no longer matches what was sealed. |
The seal block therefore takes three forms, and you must be able to tell them apart
without deducing anything from a missing field.
{ "sealed": false }{
"sealed": true,
"sealed_at": "2025-11-02T08:30:00+00:00",
"algorithm": "st-dpp-chain-v1",
"version_hash": null,
"prev_version_hash": null,
"linked": false,
"reason": "sealed_before_chain"
}{
"sealed": true,
"sealed_at": "2026-05-14T09:12:44+00:00",
"algorithm": "st-dpp-chain-v1",
"version_hash": "2222222222222222222222222222222222222222222222222222222222222222",
"prev_version_hash": "3333333333333333333333333333333333333333333333333333333333333333",
"linked": true,
"chain_link_match": true
}#Which verdict to read first
chain_link_match at false is the signal that a sealed version was
modified after its publication. It is the strongest verdict in this response.
It comes from a recomputation made at the moment of your call.
ipfs_match at false carries the same kind of signal about the public copy.
#Errors
The body of an error response carries a detail field.
| Code | Condition | What to do |
|---|---|---|
| 404 | No item matches this identifier, under any of the three accepted forms. detail is 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 public passport is published for it or for its model. detail is No published passport found for this product. | Do not treat this response as a failure of the check. There is nothing to check. A passport reserved to the owner or to the brand also gives this response. |
| 429 | The cap of 60 calls per 60 seconds is reached for your network address, across all paths starting with /passport. detail is Rate limit exceeded: 60 requests per 60s. | Wait the number of seconds given by Retry-After, then try again. |
| 500 | An unexpected error occurred during the processing of your call. detail is Internal Server Error. The response carries an X-Request-Id header. | Try again. If the error persists, contact support giving the value of X-Request-Id. |
An unreachable gateway does not produce an HTTP error. The response stays 200
and ipfs_match is null.
#See also
GET /passport/{identifier}/proof, gather the public proofs of the passport of an item.GET /passport/{identifier}, read the published passport of an item.GET /certificate/{identifier}, read the certificate of authenticity 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.