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

Source: https://docs.sealtrust.io/en/reference/get-passport-proof/

---

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:

```http
GET https://api.sealtrust.io/v1/passport/{identifier}/proof
```

The 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}`.

> [!ATTENTION] The `anchor` block does not always prove an anchoring
> This block carries either the batch root recorded on the chain, or the mint
> transaction of the item. A mint is not an anchoring. The `anchored` field
> settles between the two. Key your code on `anchored` and on `proves`. The
> name of the `anchor` key says nothing about what the reference establishes.

## Authorization

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

> [!INFO] The TypeScript SDK does not cover this endpoint
> No method of the `@sealtrust-io/sdk` package calls this address. The
> TypeScript example below uses `fetch`, with no dependency.

:::onglets
```bash title="curl"
curl -i https://api.sealtrust.io/v1/passport/1042/proof
```
```typescript
const 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());
}
```
```python
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`.

```json
{
  "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.

> [!DANGER] `data_hash_matches: false` is a tampering signal
> This value means that the passport recorded today no longer matches what was
> recorded on the chain for this version. We publish it instead of hiding it.
> Treat it as an alert and take it up with the brand.

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`](/en/reference/get-passport-verify/),
  check the integrity of the published passport of an item.
- [`GET /verify/merkle/{identifier}`](/en/reference/get-verify-merkle/),
  retrieve the proof of inclusion of an item in the batch anchored on Base.
- [`GET /passport/{identifier}`](/en/reference/get-passport-identifier/),
  read the published passport of an item.
- [Trust and proofs](/en/confiance-et-preuves/),
  what each proof establishes and how a third party redoes the verification.
