Method 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.
On this page
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:
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.
#
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.
curl -i https://api.sealtrust.io/v1/verify/merkle/1042const 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());
}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.
{
"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.
#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, gather the public proofs of the passport of an item.GET /p/{serial}, translate the printed serial number into a consumer page address.- 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.