Documentation

Quickstart

Five minutes end to end. Step 1 needs nothing from us at all — no account, no key — because you should be able to check the product before you trust it. Steps 2 onward use an API key from signup. Everything below works on the free tier.

Every command and output block on this page uses our public log api.rootwitness.com/demo, and the keys, roots and signatures shown are the ones that log is serving. The read-only parts — the checkpoint, the keys, and verifying our signature — need no credential, so you can run those against our log right now. Appending and fetching proofs need a key, so those blocks are for your own log once you have one.

1 · Verify our log before you sign up

Start here rather than with an account. The verifier is open source, Apache 2.0, and takes no credential: github.com/725ttzn82p-beep/rootwitness. Point it at our public demo log and confirm the signature yourself.

shell
pip install git+https://github.com/725ttzn82p-beep/rootwitness

rootwitness verify \
  --origin https://api.rootwitness.com/demo \
  --log-key jAJNJZDzzOuowgsy6FhDgJBriL7u0DGwuXHhxX90WXw=

Real output:

output
OK: signed checkpoint verified for https://api.rootwitness.com/demo
tree size: 12
root: gUF7FxKEkz8HK434ellhpxZV18pSLinmeiaPEyQsUKM=

This proves the operator signed this size and root, so they cannot
later deny it. It does NOT prove they have never rewritten history --
for that, run `rootwitness init` and then `check` on a schedule, so
a stored earlier checkpoint forces them to produce a consistency proof.

Change one character of the key and it exits non-zero. This proves we signed that exact size and root, so we cannot later deny it. It does not prove we have never rewritten history — that needs a witness, which is step 4 and the only part that really matters.

2 · Append a record

Signing up returns a log URL, an API key shown exactly once, and your log's public key. Appending is one HTTP call; the response is a receipt giving the sequence number and the RFC 6962 leaf hash.

shell
curl -X POST https://api.rootwitness.com/demo/append \
  -H "Authorization: Bearer $ROOTWITNESS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"agent=ops-03 tool=doc.export id=summary-2026-08 result=ok"}'
output
{
  "origin": "https://api.rootwitness.com/demo",
  "seq": 10,
  "leaf_hash": "RhxDxMurFdG+s+M5Dw0IR/mQ5THg3pvmYrVRw0jagAU=",
  "covered_by_checkpoint": false
}

covered_by_checkpoint: false is deliberate and worth understanding. The entry is durably stored, but it is not yet inside a signed checkpoint, so it is not yet evidence. Checkpoints are sealed on an interval; you can force one, which is what tests and demos should do.

shell
curl -X POST https://api.rootwitness.com/demo/seal -H "Authorization: Bearer $ROOTWITNESS_API_KEY"

# {"sealed":true,"tree_size":11}

3 · The signed checkpoint

Checkpoints are public and need no credential, because verification that required our permission would let us choose who is allowed to catch us. The format is a C2SP signed note: origin, tree size, base64 root hash, then signature lines.

shell
curl https://api.rootwitness.com/demo/checkpoint
output
https://api.rootwitness.com/demo
12
gUF7FxKEkz8HK434ellhpxZV18pSLinmeiaPEyQsUKM=

— https://api.rootwitness.com/demo fippB5FaTh8yYOyOIwmXNCx+RsiTyfvaCCc2TMzINzC2kgd8dCLZzyXQ9A/2paY+TgeOsvzr9QgMMeAIiN8w4ISFMwM=

The size and root above are whatever the log was serving when this page was written; if we have appended since, yours will be larger and the signature different. That is the point — what should match is that the signature verifies under the key above, and that a witness which saw the smaller tree still reports it consistent with the larger one.

An inclusion proof shows a specific entry is in the tree the checkpoint commits to. Hashes on the wire are base64.

shell
curl "https://api.rootwitness.com/demo/proof/inclusion?seq=3" \
  -H "Authorization: Bearer $ROOTWITNESS_API_KEY"
output
{
  "seq": 3,
  "tree_size": 11,
  "leaf_hash": "y84YNUYavjNSXJ0eo1q5uwA6QKgGsPFWkg28qpyO6N0=",
  "proof": [
    "cBvREjegznvpnxtF4LEE/il/FaZVSnPP6yXMnDM3UqA=",
    "AercvOXDDifTRH8gnmyDdnW2/QTmLFfndlTYOSQsvJ0=",
    "mxI9vvdRnAcADzDGhBVm1482aMagTbPdhSuFvp1FDeQ=",
    "APkJr8XruQIK+00WA3zzMx40OMvtZeURSywxTvzhSXg="
  ],
  "checkpoint": "…"
}

4 · Run your own witness

This is the part that matters. init generates an Ed25519 keypair on your machine, never transmits the private half, and records our current checkpoint as your baseline.

shell
rootwitness init \
  --origin https://api.rootwitness.com/demo \
  --log-key jAJNJZDzzOuowgsy6FhDgJBriL7u0DGwuXHhxX90WXw=

# witness/api.rootwitness.com+9kxK1aOq9YW6yrEwstrPpFvWdyX2roqge3AYuzRKIXo=

State lives in ~/.rootwitness by default; override with --state-dir or ROOTWITNESS_STATE_DIR. Then run check on a schedule. Each run demands a consistency proof from the size it already remembers to the size we now claim.

shell
rootwitness check

# OK: consistent through 12 entries

If that proof does not verify, the witness refuses, exits non-zero, and writes REFUSED-<timestamp>-<digest>.evidence containing the checkpoint it previously accepted and the one it just refused — both signed by us — plus what contradicts and how a third party can check it. Two signed statements from the same key that cannot both be true is the artifact you actually want; it is checkable by someone who has never heard of you.

shell
rootwitness watch --interval 300

# polls continuously; exits non-zero and writes evidence on refusal

Hash-only privacy mode

Hash the record in your own process and send only the digest to POST /<log>/append-hash. We never receive the record body. Proofs, checkpoints, and witness co-signing are unchanged, because the log only ever needed the hash.

There is no SDK to install for this and no client-side magic — it is 64 hex characters over HTTP. Canonicalize the same way every time, or the digest you compute later won't match the one you sent.

hash_only.py
import hashlib, json, os, urllib.request

record = {
    "actor": "agent:intake-v2",
    "action": "read_chart",
    "patient_ref": "mrn:88213",
    "occurred_at": "2026-08-11T14:22:09Z",
}

# Canonical JSON: sorted keys, no whitespace. Keep this identical forever,
# including in whatever re-verifies the record years from now.
canonical = json.dumps(record, sort_keys=True, separators=(",", ":")).encode()
digest = hashlib.sha256(canonical).hexdigest()   # the only thing that leaves

req = urllib.request.Request(
    f"{os.environ['ROOTWITNESS_BASE_URL']}/acme-phi-audit/append-hash",
    data=json.dumps({"sha256": digest}).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['ROOTWITNESS_API_KEY']}",
        "Content-Type": "application/json",
    },
)
receipt = json.load(urllib.request.urlopen(req))
print(receipt["seq"], receipt["leaf_hash"])

Keep canonical — or the record it came from — on your side. We are holding a 32-byte commitment and nothing else, so if you lose the original, the proof stays valid and the content is gone for good. That is the trade hash-only mode makes, and it is not reversible.

Checkpoint format

Checkpoints are C2SP signed notes: an origin line, the tree size, the base64 root hash, a blank line, then one or more signature lines. Plain text on purpose — a human can read it, and a 30-line script can verify it.

GET /demo/checkpoint
https://api.rootwitness.com/demo
12
gUF7FxKEkz8HK434ellhpxZV18pSLinmeiaPEyQsUKM=

— https://api.rootwitness.com/demo fippB5FaTh8yYOyOIwmXNCx+RsiTyfvaCCc2TMzINzC2kgd8dCLZzyXQ9A/2paY+TgeOsvzr9QgMMeAIiN8w4ISFMwM=

A witness adds a second signature line of its own. That line is your past statement that the history was consistent at that size, which is why a later contradiction is provable without our cooperation.

HTTP API

Every client operation is a plain HTTP call if you'd rather not use the library.

EndpointMethodReturns
/{log}/appendPOSTAppends a record. Returns the sequence number and RFC 6962 leaf hash.
/{log}/append-hashPOSTAppends only a digest, for payloads that must never leave your side.
/{log}/sealPOSTForces a checkpoint immediately rather than waiting for the interval.
/{log}/checkpointGETThe latest C2SP signed note. Public, no credential.
/{log}/proof/inclusion?seq=NGETAudit path proving entry N is in the tree.
/{log}/proof/consistency?old=M&new=NGETProof that size M is a prefix of size N. This is what the witness demands. Public.
/{log}/auditGETServer-side self-check. Never a substitute for your own witness.
/.well-known/rootwitness-keysGETPublic keys for every log on this host.
/signupPOSTCreates a log. Returns the API key exactly once.

CLI reference

CommandPurpose
rootwitness verifyOne-shot check of any public log's signed checkpoint. No account, no state, writes nothing.
rootwitness initGenerate the witness keypair, pin the log's public key, record a baseline checkpoint.
rootwitness checkPoll once. Demands a consistency proof, co-signs or refuses. Non-zero exit on refusal.
rootwitness watchThe same check on an interval, for a service unit.
rootwitness statusPrint the last accepted checkpoint: origin, size, root.

Alerting and CI

Treat a refusal like a production incident, because it is one.

witness.service
[Unit]
Description=Root Witness

[Service]
Environment=ROOTWITNESS_STATE_DIR=/var/lib/rootwitness
ExecStart=/usr/local/bin/rootwitness watch --interval 300
Restart=always
RestartPreventExitStatus=3        # exit 3 = consistency failure: stop and page a human
User=rootwitness

[Install]
WantedBy=multi-user.target

What this cannot do

A transparency log proves that everything in the log is intact. It cannot prove that everything that happened was written to it — omission at the boundary. If an agent acts and never calls append(), no proof downstream can surface a record that was never created.

Mitigate it where you can — append from inside the tool-execution path rather than a side channel, log the intent before the effect, and record heartbeat entries so a silent gap is visible as a gap. But understand the limit precisely: we make alterations to recorded history detectable by third parties. We make no claim to prevent alteration, we do not validate that a record is accurate, and we are not a substitute for a records-retention program.

The full limitations discussion lives on the landing page.