Home › Verify independently

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"
}

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:

  1. Take the payload object.
  2. 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.
  3. Arrays are left exactly as they are: element order is kept, and objects inside an array (for example the {"label", "value"} entries of custom_fields) keep their original key order.
  4. Serialise with JSON.stringify and no indentation: no spaces or line breaks between tokens, strings escaped the way JavaScript escapes them (non-ASCII characters are written literally, not as \u escapes).
  5. 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"

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 pricing

Sources

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.

  1. Bitcoin developer guide — null data (OP_RETURN) outputs are provably unspendable data carriers — https://developer.bitcoin.org/devguide/transactions.html
  2. Bitcoin Wiki — Block timestamp (accurate only to within an hour or two) — https://en.bitcoin.it/wiki/Block_timestamp