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

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

---

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.

> [!INFO] This response is public
> This endpoint serves the pages your end customers read. The server filters
> the passport data at the consumer tier. With the default split, the sections
> reserved for professionals are not included: manufacturing bill of
> materials, material composition beyond the primary materials, substances of
> concern, manufacturing data and supply chain. A brand that defines its own
> access rules replaces that split with its own, including for the public
> tier. An accredited professional reads those sections through another path,
> an authenticated one.

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

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `identifier` | `string` | yes | The 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.

| Order | Form | Recognized by | Case sensitive |
| --- | --- | --- | --- |
| 1 | Hash of the item | `0x` followed by 64 hexadecimal characters, that is 66 characters | no |
| 2 | Identifier of the token | Any value, compared as it stands to the recorded token identifier | yes |
| 3 | Printed serial number | 12 characters of the Crockford Base32 alphabet, which excludes the letters I, L, O and U | no |
| 4 | Certificate number | The exact value of the `certificate_number` field, for example `ST-CERT-000000000000` | yes |

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

:::onglets
```bash title="curl"
curl -i https://api.sealtrust.io/v1/resolve/0x0000000000000000000000000000000000000000000000000000000000000000
```
```typescript
const identifiant =
  "0x0000000000000000000000000000000000000000000000000000000000000000";

const response = await fetch(
  `https://api.sealtrust.io/v1/resolve/${encodeURIComponent(identifiant)}`,
);

console.log(response.status);
console.log(response.headers.get("X-RateLimit-Remaining") ?? "inconnu");
console.log(await response.json());
```
```python
import requests
from urllib.parse import quote

identifiant = "0x0000000000000000000000000000000000000000000000000000000000000000"

response = requests.get(
    f"https://api.sealtrust.io/v1/resolve/{quote(identifiant, safe='')}",
    timeout=30,
)

print(response.status_code)
print(response.headers.get("X-RateLimit-Remaining", "inconnu"))
print(response.json())
```
:::

> [!INFO] The TypeScript SDK does not cover this endpoint
> The [TypeScript SDK](/en/sdk-typescript/), package `@sealtrust-io/sdk`,
> exposes the history of an item, batch verification, the integrity check of
> the metadata, the full management of webhook subscriptions, the partner
> batch mint call and the tracking of its job. It exposes no universal
> resolution method. So you call this endpoint over direct HTTP, as above.

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

| Field | Type | Description |
| --- | --- | --- |
| `token_id` | `string` or `null` | The identifier of the token, as a string. `null` as long as the mint has not been confirmed on the chain. |
| `uid_hash` | `string` or `null` | The hash of the item, as it is recorded. |
| `product_name` | `string` or `null` | The name of the item. |
| `brand_name` | `string` or `null` | The name of the owning brand. `null` if no brand is attached. |
| `category_name` | `string` or `null` | The name of the category. `null` if no category is attached. |
| `sku` | `null` | Always `null`. The field appears in the response and is never filled in by this endpoint. |
| `description` | `null` | Always `null`. Same remark as for `sku`. |
| `metadata_uri` | `string` or `null` | The address of the metadata of the item. |
| `created_at` | `string` or `null` | Date of creation of the record, formatted as ISO 8601 in universal time. |
| `tx_hash` | `string` or `null` | The mint transaction. It does not change when the item is transferred, so it is the link of proof of origin. |
| `contract_address` | `string` or `null` | The address of the contract that carries this token. |
| `certificate` | object or `null` | The certificate currently valid. `null` if the item has none. |
| `passport` | object or `null` | The published passport, filtered at the consumer tier. `null` if no published passport exists. |
| `media` | array | The media of the item. Empty array if none. |
| `events` | array | The public history of the item. Empty array if none. |
| `merkle_anchor` | object or `null` | The proof of inclusion of the item in a batch anchored on Base. |
| `passport_anchor` | object or `null` | The 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.

| Field | Type | Description |
| --- | --- | --- |
| `certificate_number` | `string` | The number of the certificate. It is also one of the four forms of identifier accepted by this endpoint. |
| `status` | `string` | Always 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_at` | `string` | Date of issuance, formatted as ISO 8601. |
| `expires_at` | `string` or `null` | Date 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_name` | `string` or `null` | The 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.

| Field | Type | Description |
| --- | --- | --- |
| `schema_version` | `string` | The version of the data schema of the passport. |
| `passport_version` | `integer` | The version number of the passport, incremented at every publication. |
| `data` | object | The content of the passport, filtered at the consumer tier. Its structure depends on the product category and on what the brand filled in. |
| `published_at` | `string` or `null` | Date of publication of this version. |
| `data_hash` | `string` or `null` | The 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.

| Field | Type | Description |
| --- | --- | --- |
| `id` | `integer` | The identifier of the media. `0` for the last resort cover image. |
| `file_name` | `string` | The name of the file. |
| `media_type` | `string` | `image`, `video`, `document` or `3d_model`. |
| `url` | `string` | The public address of the file. The server removes from the list a media item whose address it cannot build. |
| `alt_text` | `string` or `null` | The 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.

| Field | Type | Description |
| --- | --- | --- |
| `id` | `integer` | The identifier of the event. |
| `event_type` | `string` | The 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`. |
| `description` | `string` or `null` | The free text entered by the author of the event. |
| `occurred_at` | `string` | Date of the event, formatted as ISO 8601. |
| `actor_name` | `string` or `null` | The 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.

| Field | Type | Description |
| --- | --- | --- |
| `anchor_tx_hash` | `string` | The transaction that recorded the root on Base. |
| `root` | `string` | The anchored root. |
| `leaf_index` | `integer` | The position of the leaf of this item, counted from 0. |
| `leaf` | `string` | The hash of the leaf of this item. |
| `proof` | array of `string` | The 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.

```python title="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.

| Field | Type | Description |
| --- | --- | --- |
| `tx_hash` | `string` | The transaction that recorded the root on Base. |
| `basescan_url` | `string` or `null` | The direct link to this transaction on the explorer of the chain. |
| `merkle_root` | `string` or `null` | The anchored root. |
| `anchored_at` | `string` or `null` | Date of the anchoring, formatted as ISO 8601. |
| `passport_version` | `integer` or `null` | The version of the passport covered by this anchoring. |
| `data_hash_matches` | `boolean` or `null` | `true` when the content stored today matches what was anchored. `false` signals that the passport has changed since. |

> [!ATTENTION] Treat `data_hash_matches: false` as a signal
> This value means that the passport published today is no longer the one
> whose hash was recorded on the chain. An interface that displays the
> anchoring must display this discrepancy. Hiding it would amount to
> presenting a proof that does not cover the content shown.

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

| Code | Condition | What to do |
| --- | --- | --- |
| 404 | No 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. |
| 404 | No identifier was provided, the call stops at `/v1/resolve`. Message `Not Found`. | Add the identifier in the path. |
| 405 | A 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`. |
| 429 | More 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. |
| 500 | Unexpected 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`.

> [!INFO] An anchoring problem does not make the call fail
> A failure to compute one of the two proofs of anchoring does not bring the
> call down. The field concerned then reads `null` and the server returns the
> rest of the response normally, in 200. So treat the absence of anchoring as
> an ordinary case in your interface.

## See also

- [`GET /products/{uid}/public`](/en/reference/get-products-uid-public/),
  read the public information of a product.
- [`GET /timeline/{identifier}`](/en/reference/get-timeline/),
  read the public history of a product.
- [`GET /certificate/{identifier}`](/en/reference/get-certificate/),
  read the certificate of authenticity of an item.
- [`GET /passport/{identifier}/proof`](/en/reference/get-passport-proof/),
  gather the public proofs of the passport of an item.
