# GET /verify/merkle/{identifier}

Retrieve the proof that an item belongs to the batch anchored on Base, with its leaf, its neighbor proof and the root written to the chain. Public endpoint.

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

---

You retrieve the proof that an item was part of a batch whose hash was written
to the Base chain. When you leave this page, you will know how to ask for that
proof from any item identifier, how to recompute it yourself without trusting
us, and how to tell an item that is not anchored apart from a verification
failure.

Full address:

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

The same endpoint also answers without the `/v1` prefix, at
`https://api.sealtrust.io/verify/merkle/{identifier}`. Both addresses call the
same code. Use the `/v1` form for a new integration.

> [!INFO] A 404 is the common case
> Most items belong to no anchored batch. For them, this endpoint answers 404.
> That 404 means the item is part of no anchored batch. It says nothing about
> its authenticity. The authenticity check of an item is done on another
> endpoint.

## Authorization

None, public endpoint. You send no API key, no session and no origin header. The
response is the same for everyone.

## Rate limit

30 calls per 60-second window, counted per calling network address.

This counter is shared by the paths that start with `/verify/`, such as
`/verify/batch` or `/verify/scan-log`. The calls you address to one of them
therefore eat into the budget of the others. The `/verify_any` endpoint has its
own budget, distinct from this one.

The `/v1` prefix does not create a second budget: `/v1/verify/merkle/1042` and
`/verify/merkle/1042` fill the same counter.

Every accepted response carries three headers.

| Header | Content |
| --- | --- |
| `X-RateLimit-Limit` | the limit applied over the window, here `30` |
| `X-RateLimit-Remaining` | what is left to you in the current window |
| `X-RateLimit-Reset` | the timestamp for 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 you want the proof for. Four forms are accepted, see below. |

This endpoint has no query parameter.

`identifier` accepts four forms, tried in this order.

| Form | Appearance | Where it comes from |
| --- | --- | --- |
| Label hash | `0x` followed by 64 hexadecimal characters | the hash of the identifier of the NFC chip |
| Token id | 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 `/p/{serial}` address |
| Certificate number | as the certificate carries it | the certificate of authenticity of the item |

You write the serial number in whatever case you want. We bring the characters
that look alike back to a single form before searching, so an `I` or an `L`
typed by hand finds the `1`, and an `O` finds the `0`.

### Headers

No header is required.

## Request body

None. This request has no body.

## Example request

Membership proof of the item whose token id 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/verify/merkle/1042
```
```typescript
const reponse = await fetch("https://api.sealtrust.io/v1/verify/merkle/1042");

if (reponse.status === 404) {
  console.log("Cet article ne fait pas partie d'un lot ancré.");
} else if (reponse.ok) {
  const preuve = await reponse.json();
  console.log(preuve.merkle_root);
  console.log(preuve.leaf, preuve.leaf_index);
  console.log(preuve.proof);
  console.log(preuve.basescan_url);
} else {
  console.log(reponse.status, await reponse.json());
}
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/verify/merkle/1042",
    timeout=30,
)

if response.status_code == 404:
    print("Cet article ne fait pas partie d'un lot ancré.")
elif response.ok:
    preuve = response.json()
    print(preuve["merkle_root"])
    print(preuve["leaf"], preuve["leaf_index"])
    print(preuve["proof"])
    print(preuve["basescan_url"])
else:
    print(response.status_code, response.json())
```
:::

## Example response

HTTP code `200`.

```json
{
  "anchor_batch_id": 118,
  "merkle_root": "0x1111111111111111111111111111111111111111111111111111111111111111",
  "leaf": "0x2222222222222222222222222222222222222222222222222222222222222222",
  "leaf_index": 3,
  "proof": [
    "0x3333333333333333333333333333333333333333333333333333333333333333",
    "0x4444444444444444444444444444444444444444444444444444444444444444"
  ],
  "anchor_tx_hash": "0x5555555555555555555555555555555555555555555555555555555555555555",
  "basescan_url": "https://basescan.org/tx/0x5555555555555555555555555555555555555555555555555555555555555555",
  "contract_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "network": "Base",
  "token_id": "1042",
  "verified": true,
  "lifecycle_status": "mined",
  "withdrawn": false,
  "leaf_set": "frozen"
}
```

The response has fourteen fields and nothing else.

| Field | Type | Description |
| --- | --- | --- |
| `anchor_batch_id` | `integer` | The number under which the root of the batch is written in the contract. It is the first argument to pass to the contract if you redo the verification on the chain. |
| `merkle_root` | `string` | The root written to the chain, `0x` followed by 64 hexadecimal characters. It is the reference value. |
| `leaf` | `string` | The hash of this item in the tree, `0x` followed by 64 hexadecimal characters. |
| `leaf_index` | `integer` | The position of this hash in the list of the hashes of the batch. Counting starts at 0. |
| `proof` | `string[]` | The neighboring hashes to combine with `leaf`, from the bottom up, to obtain `merkle_root`. The list is empty when the batch contains only one item. |
| `anchor_tx_hash` | `string` | The Base transaction that carries the write of the root. |
| `basescan_url` | `string` | The direct link to that transaction on the public explorer of Base. |
| `contract_address` | `string` or `null` | The address of the contract that holds the anchored root. It is the address to query if you redo the verification on the chain. The response model allows `null`, plan for that case in your code. |
| `network` | `string` | Always `Base`. The layer 2 network whose chain identifier is 8453. |
| `token_id` | `string` or `null` | The identifier of the item on the chain, written in decimal inside a character string. It is the second argument to pass to the contract. |
| `verified` | `boolean` or `null` | The result of the verification we made for you on the chain. Three possible values, `true`, `false` and `null`, see below. |
| `lifecycle_status` | `string` or `null` | The status of the item today. On this endpoint, you read `draft`, `minting`, `mined`, `written`, `burn_submitted`, `stolen` or `revoked`. The `burned`, `superseded` and `archived` statuses never appear here, see the callout below. |
| `withdrawn` | `boolean` | On this endpoint, always `false`, see the callout below. The field moves to `true` when `lifecycle_status` is `superseded` or `archived`, the two statuses that take an item out of the brand's catalog. |
| `leaf_set` | `string` | `frozen` or `recomputed`. Where the tree that produced the proof comes from, see below. |

The response also carries the `Cache-Control: no-store, max-age=0` header. A
root that has been written no longer changes, so the proof of a given item stays
the same from one call to the next. You can therefore keep it on your side.
Nothing in the response authorizes a shared cache to keep it for you.

### Recomputing the root yourself

This is the reason this endpoint exists. You do not have to take our word for
it.

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 OpenZeppelin verification convention. The neighbors are sorted at
each level, so the proof does not need to indicate a direction. A batch with a
single item returns an empty `proof` list: the leaf is then the root.

It remains for you to check that `merkle_root` really is the value written to
the chain. Open `basescan_url` to read the write transaction, or call the read
function `verifyAnchored(batchId, tokenId, proof)` of the contract at the
address `contract_address`, with `anchor_batch_id`, `token_id` and `proof`.

### What `verified` means

`verified` is the result of that same read on the chain, made by us at the
moment of the response. It takes three values, treat them differently.

`true` means that the contract confirmed the proof.

`false` means that the contract answered and refused the proof. Do not display
the item as verified in that case. Report it to the brand.

`null` means that the read on the chain did not succeed, either because the node
queried did not answer, or because the contract rejected the call. A `null` does
not call the proof into question: `leaf`, `proof` and `merkle_root` stay
verifiable by your own means.

### What `leaf_set` means

`frozen` means that the proof was produced from the list of hashes recorded at
the very moment of the write to the chain. It is the value that is
authoritative.

`recomputed` means that the batch was anchored before we recorded that list, and
that the tree was therefore rebuilt from the current items of the batch. A
change to the batch since the anchoring can make it diverge from the written
root.

> [!ATTENTION] An item withdrawn from the catalog no longer resolves here
> The write to the chain is a statement about a past moment. It stays true
> whatever happens to the item afterwards, and the proof therefore keeps
> verifying.
>
> On this endpoint, `withdrawn` is always `false`, and `lifecycle_status` is
> never `superseded`, `archived` or `burned`. The item lookup rules out in
> advance the items destroyed on the chain and the items taken out of the
> brand's catalog: they answer 404. The two fields exist for the surfaces where
> a withdrawn item stays readable.
>
> Read `lifecycle_status` to tell apart an item still being manufactured, one
> declared stolen and one revoked.

## 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 four 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 is known, but it does not have an identifier on the chain yet, or it belongs to no batch. `detail` is `No Merkle anchor for this product`. | Do not treat this response as a failure. This item is not anchored. |
| 404 | The item belongs to a batch, but that batch has never been written to the chain. `detail` is `No Merkle anchor for this product`. | Do not treat this response as a failure. Anchoring a batch is an operation that SealTrust triggers by hand, and most batches do not go through it. |
| 404 | The batch is indeed anchored, but this item has no hash in the anchored tree. `detail` is `Product is not part of the anchored Merkle tree`. | No proof can be produced for this item. Contact the brand if you expected it in the batch. |
| 409 | The batch has changed since it was written to the chain. The recomputed root no longer matches the written root. `detail` is `Merkle anchor is stale for this batch — re-anchoring required`. | We refuse to serve a proof that would fail on the chain. Report it to the brand: the batch must be anchored again. |
| 429 | The limit of 30 calls per 60 seconds is reached for your network address. `detail` is `Rate limit exceeded: 30 requests per 60s`. | Wait the number of seconds indicated by `Retry-After`, then try again. Cache the response on your side, the proof of an item does not change. |
| 500 | An unexpected error occurred while your call was being processed. `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`. |

## See also

- [`GET /passport/{identifier}/proof`](/en/reference/get-passport-proof/),
  gather the public proofs of the passport of an item.
- [`GET /p/{serial}`](/en/reference/get-p-serial/),
  translate the printed serial number into a consumer page address.
- [Trust and proofs](/en/confiance-et-preuves/),
  what each proof establishes and how a third party redoes the verification.
