Verify a proof without icalledit
A proof is only worth something if it still works when we are not around. Everything you need is in two places: the proof.json file you downloaded, and the public Bitcoin blockchain. This guide uses only standard tools. It takes about ten minutes.
icalledit does not use OpenTimestamps and there is no .ots file. Each registration has its own Bitcoin mainnet transaction, and the SHA-256 hash sits directly in an OP_RETURN output of that transaction.
What is in proof.json
You can download proof.json for every registration from your dashboard ("Download proof.json"). It looks like this:
{
"version": 1,
"algorithm": "SHA-256",
"canonicalization": "JSON.stringify of the payload with keys sorted recursively, UTF-8",
"payload": { "asset_type": "text", "platform": "icalledit.io", "title": "My idea" },
"canonicalPayload": "{"asset_type":"text","platform":"icalledit.io","title":"My idea"}",
"hash": "…64 hex characters…",
"bitcoin": { "network": "mainnet", "txId": "…64 hex characters…", "opReturnHex": "6a20…" },
"verify": "https://icalledit.io/verify-independently"
}
canonicalPayload— the exact string that was hashed. The simplest possible check is: SHA-256 of the UTF-8 bytes of this string must equalhash.payload— the exact structured record that was hashed. It always containsplatform,asset_typeandtitle, and — only if you provided them —description,full_name,date_of_birth,government_id_hash,organization,custom_fields, and for attached filesfile_hash(assha256:<hex>),file_name,file_size,file_type.hash— the SHA-256 hash of that payload, as 64 lower-case hex characters. This is the value that was written to Bitcoin.bitcoin— the network, the transaction ID (txId, also printed on your PDF certificate) andopReturnHex, the script you should find in the transaction's OP_RETURN output:6a20followed by the hash.
Treat proof.json as private: it contains your title and description in clear text.
Step 1 — Get your proof.json and transaction ID
Download the file from the dashboard and keep it with your originals. Note the transaction ID from the file or from the certificate.
Step 2 — Canonicalise the payload
Shortcut: canonicalPayload already contains the canonical string, so you can hash it directly (step 3). Re-deriving it from payload yourself is the stricter check, because it shows that the readable record and the hashed string are the same thing. Both results must agree.
The same data can be written as JSON in many ways, so the hash is defined over one canonical form:
- Take the
payloadobject. - Sort the keys of the object in ascending order (JavaScript's default string sort, i.e. by UTF-16 code unit — for the ASCII key names used here that is plain alphabetical order). Do this recursively for nested objects.
- Arrays are left exactly as they are: element order is kept, and objects inside an array (for example the
{"label", "value"}entries ofcustom_fields) keep their original key order. - Serialise with
JSON.stringifyand no indentation: no spaces or line breaks between tokens, strings escaped the way JavaScript escapes them (non-ASCII characters are written literally, not as\uescapes). - Encode the resulting string as UTF-8, without a byte-order mark and without a trailing newline. The SHA-256 of those bytes is the hash.
For example, the payload {"title":"My idea","platform":"icalledit.io","asset_type":"text"} becomes the canonical string:
{"asset_type":"text","platform":"icalledit.io","title":"My idea"}
Step 3 — Compute SHA-256
The easy way: a short Node.js script
This script loads proof.json, canonicalises payload, hashes it, hashes canonicalPayload as well, and compares both results with hash. It also writes the canonical string to canonical.txt so you can hash it again with a second, unrelated tool.
// verify.js — run: node verify.js proof.json
const fs = require('fs'), crypto = require('crypto');
const proof = JSON.parse(fs.readFileSync(process.argv[2] || 'proof.json', 'utf8'));
function sortKeys(obj) {
const out = {};
for (const k of Object.keys(obj).sort()) {
const v = obj[k];
out[k] = v && typeof v === 'object' && !Array.isArray(v) ? sortKeys(v) : v;
}
return out;
}
const sha256 = (s) => crypto.createHash('sha256').update(s, 'utf8').digest('hex');
const canonical = JSON.stringify(sortKeys(proof.payload));
fs.writeFileSync('canonical.txt', canonical); // UTF-8, no trailing newline
console.log('from payload :', sha256(canonical));
console.log('from canonicalPayload:', sha256(proof.canonicalPayload));
console.log('declared hash :', proof.hash);
const ok = sha256(canonical) === proof.hash && sha256(proof.canonicalPayload) === proof.hash;
console.log(ok ? 'MATCH' : 'NO MATCH');
macOS and Linux
# macOS
shasum -a 256 canonical.txt
# Linux
sha256sum canonical.txt
# or hash a string you typed yourself (printf adds no trailing newline; echo would)
printf '%s' '{"asset_type":"text","platform":"icalledit.io","title":"My idea"}' | shasum -a 256
Windows (Command Prompt)
certutil -hashfile canonical.txt SHA256
Windows (PowerShell)
(Get-FileHash -Algorithm SHA256 .\canonical.txt).Hash.ToLower()
If you create canonical.txt by hand, make sure your editor saves it as UTF-8 without BOM and without a final line break — a single extra byte changes the hash completely. In Windows PowerShell 5.1, Set-Content and Out-File add a newline (and sometimes a BOM), so prefer the file written by the Node script.
All tools must print the same 64-character value, and it must equal hash in proof.json.
If you attached a file
Hash the original file with the same commands (shasum -a 256 yourfile.pdf, certutil -hashfile yourfile.pdf SHA256). The result must equal the hex part of payload.file_hash. That links your file to the payload, and the payload hash links it to Bitcoin.
Step 4 — Find the transaction on any block explorer
Open a public Bitcoin explorer — for example mempool.space, blockstream.info or blockchain.com — and paste the transaction ID into the search box. Use two different explorers if you want to rule out a mistake by one of them; if you run your own Bitcoin node, bitcoin-cli getrawtransaction <txid> true shows the same data with no third party at all.
Step 5 — Read the OP_RETURN output
The transaction has an output of 0 BTC labelled OP_RETURN (some explorers say "null data"). OP_RETURN outputs are provably unspendable outputs whose only purpose is to carry a small amount of data.[23] Its data is 32 bytes — 64 hex characters. In the raw script you will see 6a20 followed by those 64 characters (the same value as bitcoin.opReturnHex in your proof file): 6a is the OP_RETURN opcode and 20 means "32 bytes follow".
Those 64 hex characters must be identical to the hash you computed in step 3. If they are, you have shown — without relying on icalledit — that the exact payload in your proof.json existed when that transaction was made.
Step 6 — Check the status: "broadcast" vs "confirmed"
- Broadcast (explorers say "unconfirmed" or "in mempool"): the transaction has been sent to the Bitcoin network but is not yet in a block. It is not permanent yet. Normally this lasts minutes to an hour or so; when the network is congested it can take longer.
- Confirmed: the transaction has been included in a block. The explorer shows the block height and the block's time. Every later block adds another confirmation and makes the record harder to reverse; after about six confirmations it is treated as final for practical purposes.
The time that counts as evidence is the time of the block. Bitcoin block timestamps are set by miners within consensus limits and are accurate only to within an hour or two[24] — precise enough for a dispute about months or years, not for one about minutes.
If a certificate shows a transaction ID that starts with SIM-, or the verification page says "simulated", no Bitcoin transaction exists and there is nothing to verify: it is not a proof.
What you have proven — and what you have not
You have proven that this exact content existed no later than the block time. You have not proven who wrote it, that it is original, or that anyone owns it, and a timestamp is not a patent, trademark or copyright registration. How much weight a court gives it depends on the court and the jurisdiction. See how timestamps compare with the alternatives.
Prefer a browser?
The verification page has a "Check a proof file" box that performs steps 2 and 3 inside your browser (it hashes canonicalPayload and also recomputes from payload, and shows both results) with the Web Crypto API. The file is not uploaded. It is convenient — but this page exists so that you never have to trust it.
Need dated evidence today?
One registration is $19, one-time. You get a Bitcoin transaction, a PDF certificate and a proof.json you can verify without us.
Timestamp your idea → See pricingSources
Official and primary sources for the facts on this page. All accessed on 20 September 2026. Fees and processing times change — check the source before you rely on a number.
- Bitcoin developer guide — null data (OP_RETURN) outputs are provably unspendable data carriers — https://developer.bitcoin.org/devguide/transactions.html
- Bitcoin Wiki — Block timestamp (accurate only to within an hour or two) — https://en.bitcoin.it/wiki/Block_timestamp