# GET /passport/{identifier}/verify

Check the integrity of the published passport of an item: recorded hash, immutable public copy and version seal. Public endpoint.

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

---

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:

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

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

> [!INFO] This endpoint does not return the content of the passport
> It returns integrity verdicts and hashes only. To read the data of the
> passport, call
> [`GET /passport/{identifier}`](/en/reference/get-passport-identifier/). To
> obtain the public proofs gathered together, including the anchoring on the Base chain,
> call [`GET /passport/{identifier}/proof`](/en/reference/get-passport-proof/).

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

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

> [!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 --max-time 30 https://api.sealtrust.io/v1/passport/0ABCDEFGHJKM/verify
```
```typescript
const 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());
}
```
```python
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`.

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

```json
{ "sealed": false }
```

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

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

> [!ATTENTION] `db_hash_match` is the weakest of the three verdicts
> It compares the recorded data with the hash recorded next to
> it. Both values are on our side. This verdict detects corruption,
> it proves nothing against us. `ipfs_match` and `chain_link_match` are the
> two verdicts a third party can set against something we cannot
> rewrite.

## 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`](/en/reference/get-passport-proof/),
  gather the public proofs of the passport of an item.
- [`GET /passport/{identifier}`](/en/reference/get-passport-identifier/),
  read the published passport of an item.
- [`GET /certificate/{identifier}`](/en/reference/get-certificate/),
  read the certificate of authenticity of an item.
- [Trust and proofs](/en/confiance-et-preuves/),
  what each proof establishes and how a third party redoes the verification.
