Say your app needs to prove a file existed at a certain point, before anyone disputes it. The pattern is the same no matter which service you pick: hash the file locally with SHA-256, send only that hash to a timestamping service, store whatever comes back, and verify later. The file itself never has to leave your server. What you get back proves the file (or, more precisely, its hash) existed no later than the time it was anchored. It does not prove who created the file, what device captured it, or what the content means. That distinction matters once a timestamp ends up in front of a judge, an auditor, or a counterparty's lawyer.

This walks through the three kinds of API you'll run into, then wires one up end to end.

Step 1: hash the file locally

Every option downstream starts here. You compute a SHA-256 digest and that's the only thing that leaves your machine.

import hashlib

def sha256_of(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

digest = sha256_of("contract.pdf")
print(digest)

Nobody downstream sees contract.pdf. They see a 64-character hex string. That's the whole point: the timestamping service can prove a hash existed at a given time without ever holding, storing, or being trusted with the underlying file.

Step 2: choose the kind of timestamping API

Three shapes show up when you search for this.

RFC 3161 timestamping authorities. You build a TimeStampReq containing the hash, send it to a TSA, and get back a signed TimeStampToken. openssl has both ends of this built in:

openssl ts -query -digest $(sha256sum contract.pdf | cut -d' ' -f1) \
  -sha256 -cert -out request.tsq

curl -s -H "Content-Type: application/timestamp-query" \
  --data-binary @request.tsq https://<tsa-endpoint> -o response.tsr

openssl ts -verify -in response.tsr -queryfile request.tsq \
  -CAfile tsa-chain.pem

The trust model here is a certificate chain. You're trusting that specific authority's signing key, and anyone who wants to check your timestamp later needs that authority's certificate chain available to verify against. No blockchain is involved.

Hosted blockchain anchoring APIs. A vendor runs the chain interaction for you: you submit a hash through their API, and on the back end they batch it into a transaction on a public chain, store the transaction reference against your hash, and hand you a way to look that reference up later. What you're trusting is less the chain itself (that part is public and checkable) and more the vendor's record of which transaction corresponds to your hash, and whether their lookup page stays up long enough to matter. Fees, batching schedules, and what chain they use vary by vendor, so check each one's own documentation for those specifics.

ProofLedger's API. Same hash-first pattern, anchored to two chains instead of one, with no vendor dashboard standing between your hash and the record.

Step 3: submit the hash with ProofLedger's API

ProofLedger has exactly one endpoint for creating a proof. You get an API key from Account > API Keys, on any plan including the free one.

curl -X POST https://proofledger.io/api/v1/proof \
  -H "Authorization: Bearer sk_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "sha256": "1c1966d4fc20908a14df291a245e60e321a19858db5af3174f34f6c8a7f8dc96",
    "filename": "contract.pdf",
    "bitcoin_requested": false
  }'

Or from Python with requests:

import requests

resp = requests.post(
    "https://proofledger.io/api/v1/proof",
    headers={"Authorization": "Bearer sk_YOUR_KEY_HERE"},
    json={
        "sha256": digest,
        "filename": "contract.pdf",
        "bitcoin_requested": False,
    },
)
proof = resp.json()
proof_id = proof["id"]
verification_url = proof["verification_url"]

A successful call returns 201 with id, sha256, status, created_at, duplicate_of (set if you've already anchored this exact hash), certificate_url, and verification_url. Polygon anchoring happens right away. Set bitcoin_requested: true and the hash also goes into the next daily Bitcoin batch.

Store id and verification_url next to whatever record in your own database this proof belongs to. That's the thing you'll hand to an auditor or opposing counsel later, not the hash by itself.

If you'd rather not write the HTTP calls yourself, the verify-proof package wraps this same endpoint:

pip install verify-proof
PROOFLEDGER_API_KEY=sk_YOUR_KEY_HERE verify-proof create contract.pdf

That prints the proof id, status, certificate URL, and verification URL, same as the raw API call.

Step 4: check status and verify

Fetch the current state of a proof with your key:

curl https://proofledger.io/api/v1/proof/34 \
  -H "Authorization: Bearer sk_YOUR_KEY_HERE"

That returns status plus chain_tx, chain_name, and, once the daily batch runs, bitcoin_txid and bitcoin_explorer_url.

The more useful endpoint for anyone who isn't you is the public verify path. No key, no account, rate-limited to protect the service:

curl "https://proofledger.io/api/v1/verify?hash=1c1966d4fc20908a14df291a245e60e321a19858db5af3174f34f6c8a7f8dc96"

That's the call opposing counsel, an auditor, or any third party can run themselves. It returns found, the matching sha256, the proof record, and explorer links for both chains where applicable. You don't have to vouch for the result. They can check it directly.

For a fully offline check, verify-proof also ships hash and verify subcommands that never touch the network, plus four MCP tools (compute_file_hash, verify_file, verify_hash, explain_proof) for the same offline checks from inside an agent or editor. Only create and its matching create_proof MCP tool talk to ProofLedger, and both send a digest, never a file.

Which approach fits your app

Each option asks you to hold a different piece of trust.

An RFC 3161 authority asks you to hold and distribute a certificate chain, and to build or vendor the verification logic yourself. Anyone checking your timestamp later has to trust that authority's key, and have its chain on hand, for as long as the record matters.

A hosted blockchain anchoring API asks you to trust that vendor's record of which chain transaction maps to your hash, and that their lookup page keeps working for as long as the record might matter. The chain data itself is public; the mapping to your hash runs through their system.

ProofLedger anchors the same hash to two chains instead of one, and the verification path doesn't route back through a dashboard you have to keep believing. GET /api/v1/verify is public, takes no key, and returns the same answer to your app, to an auditor, or to someone on the other side of a dispute. Add the verify-proof package and the offline checks don't call any network at all. If you're building proof-of-existence into an app where someone besides you will eventually need to check the timestamp, that's the piece worth weighing before you pick an API.