proven on the hosted testnet instance · 2026-09-13 · QA-14
Verify a file by hash, with no account
What you will have at the end
Section titled “What you will have at the end”A provenance verdict for a hash, from a call that carries no Authorization
header at all — and, when the answer is yes, the on-chain receipt to open.
This is the route you hand to somebody who has no relationship with Permafrost and no reason to acquire one. A counterparty, an auditor, a user checking a file they were sent. They need the bytes, or just the hash, and nothing else.
Prerequisites
Section titled “Prerequisites”- A sha256 hash, or the bytes to compute one from.
shasum -a 256 <file>on a Mac,sha256sum <file>on Linux. curl, and the instance up (The testnet instance).- No account. No key, no token, no invite. That is the point of the page.
The call
Section titled “The call”Both calls below are deliberately keyless — no Authorization header on either.
# Verification takes no key: no Authorization header on either call below.# Anyone holding the bytes can ask whether Permafrost holds provenance.## Verify indexes the STORED hash - the bytes as stored - so the first call# answers provenance true. Asking with CONTENT_HASH, the plaintext hash,# legitimately answers provenance false: the plaintext hash is what the# certificate names, not what storage indexes.
curl -sS -m 60 "$HOST/v1/api/verify?hash=$STORED_HASH"echo
curl -sS -m 60 "$HOST/v1/api/verify?hash=$CONTENT_HASH"echoThe hash goes in the query string as 64 hex characters. A 0x prefix and
upper-case letters are normalised for you; anything that is not a sha256 after
that is answered 400 {"error": "invalid_hash"}.
What comes back
Section titled “What comes back”When Permafrost holds provenance for those bytes:
{ "hash": "<the normalised hash you asked with>", "provenance": true, "nft_id": "0x…", "content_hash": "<the fingerprint anchored on chain>", "certified": true, "explorer_url": "https://…/object/0x…"}provenanceis the verdict, and it is the only field that is always there.nft_idis the on-chain provenance anchor — the receipt object. Empty if nothing has been minted for that record yet.content_hashis the fingerprint the record carries.certifiedis the registration status: true once the object has reached the storage layer’s certified state. A record can be held withcertified: false— the provenance exists, the certification is still catching up.explorer_urlresolvesnft_idon a Sui explorer. Empty whennft_idis. Following it is Read your receipt on Sui.
When it does not:
{ "hash": "…", "provenance": false }Two fields, nothing else. There is no “close match”, no partial answer and no
error — a provenance: false is a clean, complete answer.
What it deliberately will not tell you
Section titled “What it deliberately will not tell you”Not the owner, not the tenant, not the bucket or the key, not the size, not how many times it has been read, not where the bytes are stored. Those fields exist on the record and none of them is ever mapped into this response.
If several stored objects carry the same bytes, you get one canonical record — and no hint that there are others, no count, and no enumeration of who holds them. A route anybody can call cannot double as a directory of who stored what.
Why a plaintext hash can answer false
Section titled “Why a plaintext hash can answer false”The verify route indexes the bytes the storage layer received. Which bytes those are depends on how the material got there, and that is the whole of the confusion this section exists to prevent.
For an object you stored through the S3 surface, the bytes the storage layer received are the bytes you sent. Hash your local file and ask with that; it matches. The fingerprint is taken before anything is encrypted at rest, so encryption does not change the answer.
For something held in custody, it does not. Custody hands the storage layer
ciphertext, so the indexed hash is the ciphertext’s — stored_hash in that
surface’s answers. The plaintext hash, content_hash, is the one the
certificate names, and asking verify with it legitimately returns
provenance: false. That is the wrong hash for this route, not a missing record.
The second call in the block above demonstrates exactly that. See Hold, serve,
incinerate.
One more case: an object you deleted no longer answers. Deletion tombstones the record, and a tombstoned record is not indexed. The receipt on Sui is untouched by that — it does not depend on this instance at all.
How you know it worked
Section titled “How you know it worked”The first call prints "provenance":true with an nft_id and an
explorer_url; the second prints "provenance":false. Two different answers
from the same route, with no credential on either, is the whole demonstration.
What to read next
Section titled “What to read next”- Read your receipt on Sui — open the
nft_idand read what it holds. - A storage receipt is not a provenance
receipt — what a
provenance: trueis and is not saying. - Headers, status codes, 401 and 429 — the exact codes this route answers, and the ones every other route answers.
Permafrost runs on Sui testnet and Walrus testnet. Everything here describes a shipped testnet instance, not a production service.