You have a file and you want a record, made now, that someone else can check years from now: this exact file existed by this date and has not changed since. You do not want to hand over the file to get it. OpenTimestamps and ProofLedger both do that by hashing the file where it sits and anchoring the SHA-256 hash in a public blockchain transaction. The difference is what you hold afterwards, and what the person checking it has to run.

What OpenTimestamps does

OpenTimestamps is an open protocol and an open-source client. Nothing is hosted for you beyond the public calendar servers.

The flow, from the client's documentation:

1. ots stamp contract.pdf hashes the file and sends the hash to one or more calendar servers. It writes a receipt next to the file, contract.pdf.ots. 2. Each calendar collects hashes from many users into a Merkle tree and commits the root in a Bitcoin transaction on its own schedule. 3. At first the receipt holds a pending attestation: a promise from the calendar that a Bitcoin commitment is coming. Once that transaction confirms, ots upgrade contract.pdf.ots fetches the path from your hash up to the committed root and writes it into the receipt. 4. ots verify contract.pdf.ots recomputes the file's hash, walks the path in the receipt, and checks the result against Bitcoin block data. The client needs a source for that data, such as a local Bitcoin node; its README covers the options.

pip install opentimestamps-client
ots stamp contract.pdf
ots upgrade contract.pdf.ots
ots verify contract.pdf.ots

Where the record lives: in the Bitcoin transaction the calendar made, plus the .ots receipt you keep. The receipt is the only thing that ties your hash to that transaction, because your hash is one leaf in a tree and only the root went on chain. Lose the receipt and the on-chain root still exists, but nothing connects your file to it.

What you depend on: the calendar servers, until the upgrade step has completed. If a calendar is unreachable before you upgrade, the pending receipt cannot be completed from anywhere else. After the upgrade, the receipt stands on its own and the calendar is no longer part of the check.

What there is not: an account, an API key, a hosted page, a webhook, or a URL you can send to someone. The receipt file and the client are the whole product.

What ProofLedger does

ProofLedger is a hosted API in front of the same idea. You hash the file on your machine, send the hash, and get back a proof that is anchored in its own Polygon transaction, usually within a minute. Only the hash leaves your machine, plus a file name unless you leave it out.

sha256sum contract.pdf
# 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08  contract.pdf

curl -X POST https://proofledger.io/api/v1/proof \
  -H "Authorization: Bearer sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "filename": "contract.pdf", "external_id": "contract-2026-0142"}'

The response is 201 with the proof's id, the sha256 you sent, polygon_status (PENDING until the transaction is recorded), and two public links: certificate_url and verification_url. external_id is your own record id; sending the same one again returns the existing proof with 200 instead of creating a second one.

Anchoring is asynchronous. Poll GET /api/v1/proof/{id} until polygon_status is ANCHORED and chain_tx holds the transaction hash, or register a webhook with POST /api/v1/webhooks and receive the proof.polygon.anchored event. Then save the bundle next to the file:

curl -H "Authorization: Bearer sk_your_key" \
  "https://proofledger.io/api/v1/proof/42/bundle?download=1" -o contract.pdf.proof.json

Where the record lives: in the Polygon transaction, plus the bundle. The bundle is one JSON document with the hash, the chain, the transaction id and the anchor time, in the format the open-source verify-proof tool reads. ProofLedger's certificate page, its database and its public verify endpoint are convenience views of that same transaction; none of them is required to check the proof.

Before 15 October 2026 ProofLedger also anchored some proofs on Bitcoin in daily batches; bundles from that period carry a Merkle path, and proofs made since are Polygon only.

One leaf in a tree, or one transaction per hash

This is the structural difference, and it decides what the receipt has to contain.

OpenTimestamps batches. Your hash is combined with sibling hashes up to a root, and the root is what the chain holds. So the receipt must carry the path, and a verifier must recompute it.

ProofLedger writes one transaction per proof. The hash itself is in the transaction, so there is no path to recompute. The bundle's merkle_path field is empty for every Polygon proof. Verification is a direct comparison: hash the file, compare it with the hash in the bundle, confirm the transaction on a block explorer.

Neither design is better in the abstract. Batching lets one transaction cover many hashes. One transaction per hash removes a step from the check and lets anyone find the record from the hash alone, which is what the public verify endpoint does.

Checking each one without the issuer

For OpenTimestamps, the check is ots verify against the receipt. No calendar is involved after the upgrade. You need the file, the receipt, the client and Bitcoin block data.

For ProofLedger, the check is verify-proof against the bundle, with no network and no account:

pip install verify-proof
verify-proof verify contract.pdf --proof contract.pdf.proof.json

From Python:

from verify_proof import hash_file, load_proof, verify_proof

file_hash = hash_file("contract.pdf")
bundle = load_proof("contract.pdf.proof.json")
result = verify_proof(file_hash, bundle)

print(result["verified"])     # True: the file's hash matches the bundle and the bundle names a transaction
print(result["blockchain"])   # "polygon"
print(result["tx_id"])        # the transaction to look up on a block explorer
print(result["anchored_at"])  # the anchor time the bundle records
print(result.get("message") or result.get("error"))

Be precise about what this does. It recomputes the file's SHA-256 and compares it with the hash in the bundle. If the two differ, the file is not the one the proof was made for, and verified is False with an error. If they match and the bundle names a transaction, verified is True and the message tells you which transaction to look up. It does not read the chain. The anchor time is what the bundle says until you open the transaction on a block explorer such as Polygonscan and see the hash and the block time there. If the bundle carries a Merkle path, which only the older Bitcoin-era bundles do, it computes the root for you to compare against the transaction.

A third party with a hash and no bundle can also ask the public endpoint, with no key:

curl "https://proofledger.io/api/v1/verify?hash=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"

It answers found plus, when found, the proof's chain_tx and an explorer_url. It is rate limited to 120 requests an hour per IP. It is a convenience, not the record: the record is the transaction.

What both proofs show

The same thing. A file with this exact hash existed by the time of the anchoring transaction, and the file you hold now matches it byte for byte. Neither proof says who made the file, whether its contents are true, or who owns it. Whether a court accepts any of this is for the court and the lawyers.

Which one fits

Pick OpenTimestamps when you want no vendor at all, you are comfortable running the client and the upgrade step, you can keep .ots receipts next to their files, and whoever checks the proof later can run the client too.

Pick ProofLedger when you want an HTTP call from your own code, a response you can store by your own record id, a webhook when the anchor lands, a bundle in a documented format, and a URL or public endpoint for people who will not install anything. The trade is that a service sits between you and the chain at submission time. After the transaction is recorded, it does not.

To check a ProofLedger proof without ProofLedger, keep the file and its bundle: run verify-proof verify to confirm the file matches, then open the bundle's tx_id on any Polygon block explorer to confirm the hash and the block time. The API is documented at https://proofledger.io/api.html.