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.
pip install git+https://github.com/725ttzn82p-beep/rootwitness
rootwitness verify \
--origin https://api.rootwitness.com/demo \
--log-key jAJNJZDzzOuowgsy6FhDgJBriL7u0DGwuXHhxX90WXw=
Real 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.
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"}'
{
"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.
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.
curl https://api.rootwitness.com/demo/checkpoint
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.
curl "https://api.rootwitness.com/demo/proof/inclusion?seq=3" \
-H "Authorization: Bearer $ROOTWITNESS_API_KEY"
{
"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.
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.
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.
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.
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.
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.
| Endpoint | Method | Returns |
|---|---|---|
| /{log}/append | POST | Appends a record. Returns the sequence number and RFC 6962 leaf hash. |
| /{log}/append-hash | POST | Appends only a digest, for payloads that must never leave your side. |
| /{log}/seal | POST | Forces a checkpoint immediately rather than waiting for the interval. |
| /{log}/checkpoint | GET | The latest C2SP signed note. Public, no credential. |
| /{log}/proof/inclusion?seq=N | GET | Audit path proving entry N is in the tree. |
| /{log}/proof/consistency?old=M&new=N | GET | Proof that size M is a prefix of size N. This is what the witness demands. Public. |
| /{log}/audit | GET | Server-side self-check. Never a substitute for your own witness. |
| /.well-known/rootwitness-keys | GET | Public keys for every log on this host. |
| /signup | POST | Creates a log. Returns the API key exactly once. |
CLI reference
| Command | Purpose |
|---|---|
| rootwitness verify | One-shot check of any public log's signed checkpoint. No account, no state, writes nothing. |
| rootwitness init | Generate the witness keypair, pin the log's public key, record a baseline checkpoint. |
| rootwitness check | Poll once. Demands a consistency proof, co-signs or refuses. Non-zero exit on refusal. |
| rootwitness watch | The same check on an interval, for a service unit. |
| rootwitness status | Print the last accepted checkpoint: origin, size, root. |
Alerting and CI
Treat a refusal like a production incident, because it is one.
[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.