API Documentation
Public and authenticated JSON endpoints.
Overview
All responses are JSON. Public endpoints require no credentials. Authenticated endpoints require a Bearer API key.
Base URL:
https://kerneloftruth.org/api.php
Authentication
Generate an API key from your Profile page. Keys begin with kot_ followed by 48 hex characters. Send your key with every authenticated request:
Authorization: Bearer kot_your_key_here
Keys are shown only once at creation. Revoke any key at any time from your profile.
Public endpoints
No authentication required.
?action=check_hash&hash={sha256}
Check whether a SHA-256 hash has been claimed.
Example request:
https://kerneloftruth.org/api.php?action=check_hash&hash=e3b0c44298fc1c149afb...
Response (claimed):
{
"claimed": true,
"public_id": "abc123xyz",
"claimer": "Jane Smith",
"claimed_at": "2025-06-01T14:32:00Z",
"verify_url": "https://kerneloftruth.org/verify/abc123xyz"
}
Response (not claimed):
{
"claimed": false
}
?action=claim&id={public_id}
Get full details for a claim by its public ID.
Example response:
{
"public_id": "abc123xyz",
"title": "My original essay",
"description": "First draft, published June 2025.",
"content_type": "text",
"content_hash": "e3b0c44298fc1c149afb4c8996fb92427ae41e4649b934ca495991b7852b855",
"hash_algorithm": "sha256",
"claimer": "Jane Smith",
"user_id": 42,
"claimed_at": "2025-06-01T14:32:00Z",
"verify_url": "https://kerneloftruth.org/verify/abc123xyz"
}
?action=user&id={user_id}
Get a public user profile and their recent claims.
Example response:
{
"user_id": 42,
"name": "Jane Smith",
"bio": "Writer and researcher.",
"claim_count": 17,
"recent_claims": [
{
"public_id": "abc123xyz",
"title": "My original essay",
"content_type": "text",
"claimed_at": "2025-06-01T14:32:00Z",
"verify_url": "https://kerneloftruth.org/verify/abc123xyz"
}
]
}
?action=find_similar
Find claims whose content is substantially the same as yours, rather than
byte-identical. You compute the fingerprint yourself β the content is never sent.
Use assets/js/fingerprint.js in a browser, or port
includes/fingerprint.php.
fingerprint Unsigned 64-bit decimal integer (required) kind "text" or "image" (default: text) max_distance Hamming distance ceiling, clamped to 11 (default: 10)
Example response:
{
"kind": "text",
"max_distance": 10,
"count": 1,
"matches": [
{
"id": "abc123xyz",
"title": "My original essay",
"distance": 4,
"similarity": 94,
"exact_match": false,
"confidence": "likely",
"verify_url": "https://kerneloftruth.org/verify/abc123xyz"
}
],
"truncated": false
}
Limits worth knowing. This detects reformatting, light editing and partial reuse. It does not detect thorough rewording: a full paraphrase is lexically indistinguishable from unrelated text, so no threshold separates them. Only claims whose owner opted into fingerprinting are searchable. Rate limited to 20 requests per minute per IP.
?action=attestations&id={public_id}
List third-party attestations β other accounts confirming or disputing a claim.
{
"claim_id": "abc123xyz",
"confirms": 2,
"disputes": 0,
"attestations": [
{
"stance": "confirms",
"statement": "I was present when this was said.",
"attestor": {
"id": 99,
"name": "Alex Doe",
"verified_identities": 3
},
"created_at": "2026-07-01T09:14:00Z"
}
]
}
?action=check_hashes
Bulk registration check. Send hashes as a JSON array, or as a
comma/newline-separated list so a pasted spreadsheet column works.
Maximum 100 per request.
curl -X POST "https://kerneloftruth.org/api.php?action=check_hashes" \ -d 'hashes=["e3b0c442...","a1b2c3d4..."]'
{
"count": 2,
"registered": 1,
"results": [
{
"hash": "e3b0c442...",
"registered": true,
"state": "registered",
"public_id": "abc123xyz",
"claimed_at": "2026-07-01 09:14:00",
"claimed_by": "Alex Doe",
"verify_url": "https://kerneloftruth.org/verify/abc123xyz"
},
{ "hash": "a1b2c3d4...", "registered": false }
]
}
state distinguishes registered,
disclaimed (the owner withdrew the claim) and
revoked (administratively removed) β a bare
“not registered” would conflate all three.
Rate limited to 30 batches per hour per IP on top of the global limit.
?action=receipt&hash={sha256}
Issue a verification receipt: a signed statement of what this server saw for a hash at a moment in time. Unlike a page view, a receipt is transferable β you can show it to a third party, who can check the signature without trusting this site.
{
"receipt_id": "9f2a4c1b7e3d5a8f6b0c2d4e",
"verdict": "registered",
"url": "https://kerneloftruth.org/receipt/9f2a4c1b7e3d5a8f6b0c2d4e",
"payload_signed": "{\"version\":1,\"receipt_id\":\"9f2a...\",...}",
"signature": "base64...",
"key_id": "3c9f1b...",
"public_key_url": "https://kerneloftruth.org/.well-known/kot-receipt-key"
}
Verify against payload_signed β the exact bytes that were
signed β never a re-serialisation of the decoded object, since JSON key
order and escaping are not guaranteed stable.
# Verify a receipt offline curl -s "https://kerneloftruth.org/.well-known/kot-receipt-key?format=pem" -o key.pem jq -r .payload_signed receipt.json | tr -d '\n' > payload.txt jq -r .signature receipt.json | base64 -d > sig.bin openssl dgst -sha256 -verify key.pem -signature sig.bin payload.txt
A receipt attests to registration state only β never to authorship, ownership, or originality. That limitation is stated inside the signed payload, so it travels with the receipt. Rate limited to 30 per hour per IP.
?action=receipts
Bulk receipt issuance, for auditing a batch. Same hashes
input as check_hashes. Maximum 25 per request
(each receipt is an RSA signature), 10 batches per hour per IP.
?action=revisions&id={public_id}
The full revision chain for a claim. Documents get revised, and the lineage is often more informative than any single timestamp.
{
"claim_id": "abc123xyz",
"revision": 2,
"total": 3,
"versioned": true,
"revisions": [
{ "revision": 1, "public_id": "aaa111", "title": "Draft 1",
"claimed_at": "2026-06-01 10:00:00", "is_requested": false },
{ "revision": 2, "public_id": "abc123xyz", "title": "Draft 2",
"claimed_at": "2026-06-15 12:00:00", "is_requested": true },
{ "revision": 3, "public_id": "ccc333", "title": "Final",
"claimed_at": "2026-07-01 09:00:00", "is_requested": false }
]
}
Each revision is an independent timestamped claim. A later revision does not invalidate or replace an earlier one, and no timestamp changes when a revision is added.
?action=disputes&id={public_id}
Open disputes recorded against a claim.
{
"claim_id": "abc123xyz",
"disputed": true,
"disputes": [
{
"dispute_id": 12,
"filed_by": "Sam Rivera",
"filed_at": "2026-07-10 08:22:00",
"statement": "This reproduces work I registered earlier.",
"evidence_count": 3,
"counter_claim": "https://kerneloftruth.org/verify/zzz999",
"record_url": "https://kerneloftruth.org/dispute/12"
}
]
}
Kernel of Truth records disputes and does not adjudicate them. A dispute is not a finding against the claim β do not present a dispute count as an adjudicated outcome.
Transparency log
Every claim here is individually verifiable, but that alone still asks you to trust that the operator has not deleted a claim, altered a hash, or backdated a timestamp. So the whole registry is periodically reduced to a single Merkle root, chained to the previous root and anchored to Bitcoin via OpenTimestamps.
Alter any covered claim afterwards and the root no longer matches. Because the old root is anchored in Bitcoin, what it used to be is provable β without trusting this server.
/transparency.php?format=json
The published root list.
{
"issuer": "https://kerneloftruth.org",
"leaf_format": "sha256(\"leaf:\" + id|public_id|content_hash|created_at)",
"node_format": "sha256(\"node:\" + left_digest + right_digest)",
"odd_node": "promoted unchanged, never duplicated (CVE-2012-2459)",
"roots": [
{
"id": 42,
"root_hash": "bb1fcc3481f44d27...",
"prev_root": "a7c9e2f10b83d456...",
"from_claim_id": 1,
"to_claim_id": 5821,
"leaf_count": 5794,
"published_at": "2026-07-29 03:00:00",
"bitcoin": { "submitted_at": "2026-07-29 03:00:04",
"anchored": true, "block_height": 925017 },
"audit_url": "https://kerneloftruth.org/transparency.php?audit=42"
}
]
}
/ots.php?id={public_id}&verify=1
Verify a claim's OpenTimestamps proof against Bitcoin. The proof's operations are replayed to a merkle root and compared against the real block header β this is not a restatement of stored state.
{
"claim_id": "abc123xyz",
"status": "verified",
"detail": "The proof commits to the merkle root of Bitcoin block 925017.",
"block_height": 925017,
"computed_merkle_root": "8a1b66ecb7cbd07d...",
"block_merkle_root": "8a1b66ecb7cbd07d...",
"proof_url": "https://kerneloftruth.org/ots.php?id=abc123xyz"
}
status is one of verified,
pending (not in a block yet), unconfirmed
(a block explorer could not be reached β a failure of our
check, not of the proof), or invalid (the proof
does not commit to the block it names). Rate limited to 30 per hour
per IP. For a check that trusts nobody, download the
.ots file and run ots verify locally.
How to verify a root yourself
The tree is deliberately simple enough to rebuild by hand:
-
Fetch every active claim in the root's
from_claim_id–to_claim_idrange, ordered by claim ID ascending. -
Build each leaf as the canonical pipe-joined string, then hash it with a
leaf:prefix:leaf_string = id + "|" + public_id + "|" + content_hash + "|" + created_at leaf_digest = SHA256("leaf:" + leaf_string) -
Combine adjacent pairs upward, prefixing internal nodes with
node::node_digest = SHA256("node:" + left_digest + right_digest) - If a level has an odd number of nodes, promote the last one unchanged to the next level. Do not duplicate it.
- Repeat until one digest remains. Hex-encode it and compare.
The leaf: / node: prefixes are domain separation β
without them a crafted leaf could impersonate an internal node. Promoting
rather than duplicating an odd node avoids the Bitcoin malleability bug
(CVE-2012-2459), where [a,b,c] and [a,b,c,c]
produce the same root; if that were possible, a tampered registry could be
made to match a published anchor.
created_at and public_id are inside the leaf on
purpose. A tree over content_hash alone would not detect a
backdated timestamp, which is the tampering hardest to spot by eye and the
most valuable to catch in a priority dispute.
# Reference implementation (Python 3)
import hashlib
def leaf(c):
s = f"{c['id']}|{c['public_id']}|{c['content_hash']}|{c['created_at']}"
return hashlib.sha256(b"leaf:" + s.encode()).digest()
def merkle_root(claims):
level = [leaf(c) for c in sorted(claims, key=lambda c: c['id'])]
if not level:
return None
while len(level) > 1:
nxt = []
for i in range(0, len(level), 2):
if i + 1 < len(level):
nxt.append(hashlib.sha256(b"node:" + level[i] + level[i+1]).digest())
else:
nxt.append(level[i]) # promote, never duplicate
level = nxt
return level[0].hex()
Each root's audit_url makes this server recompute the tree and
report whether it still matches β useful as a cross-check, though the point
is that you never have to take our word for it.
Authenticated endpoints
All requests must include Authorization: Bearer kot_...
?action=my_claims
List your claims. Supports the following query parameters:
p Page number (default: 1) limit Results per page, max 100 (default: 20) q Search by title or description type Filter by type: text, document, image, other
Example response:
{
"claims": [ ... ],
"total": 17,
"page": 1,
"limit": 20
}
?action=create_claim
Register a new claim. Send a JSON body:
{
"hash": "e3b0c44298fc1c149afb4c8996fb92427ae41e4649b934ca495991b7852b855",
"title": "My original essay",
"content_type": "text",
"description": "Optional description.",
"fingerprint": "204026503994625523",
"fingerprint_kind": "text"
}
The hash field must be a 64-character lowercase SHA-256 hex string.
fingerprint is optional β supply it to make the claim findable by
near-duplicate search. Success response (HTTP 201):
{
"id": "abc123xyz",
"title": "My original essay",
"content_type": "text",
"verify_url": "https://kerneloftruth.org/verify/abc123xyz",
"badge_url": "https://kerneloftruth.org/badge.php?id=abc123xyz"
}
?action=create_claims
Register up to 100 claims in one request. Each entry takes the same
fields as create_claim.
{
"claims": [
{ "hash": "e3b0c442...", "title": "Essay one", "content_type": "text" },
{ "hash": "a1b2c3d4...", "title": "Essay two", "content_type": "text" }
]
}
Partial success is the contract. Every entry gets its own result, so
one already-claimed hash does not fail the batch. The status code is
201 when all succeeded, 207 when results are mixed, and
422 when none succeeded.
{
"created": 1,
"failed": 1,
"results": [
{ "index": 0, "status": "created", "id": "abc123xyz",
"verify_url": "https://kerneloftruth.org/verify/abc123xyz" },
{ "index": 1, "status": "conflict",
"error": "This hash has already been claimed.",
"claimed_by": "Jane Smith" }
]
}
Each entry counts toward the same hourly per-account claim ceiling as a single call, so bulk is a convenience rather than a way around the limit.
?action=stamp&id={public_id}
Anchor a claim to Bitcoin via OpenTimestamps. Returns immediately with a calendar commitment; Bitcoin confirmation completes asynchronously.
{
"claim_id": "abc123xyz",
"status": "submitted",
"proof_url": "https://kerneloftruth.org/ots.php?id=abc123xyz",
"verify_url": "https://kerneloftruth.org/ots.php?id=abc123xyz&verify=1"
}
Idempotent β an already-stamped claim returns
status: already_stamped rather than an error, so a re-run is
safe. Any account may stamp any claim: it costs the owner nothing and
strengthens the public record. 20 per hour per IP.
?action=disclaim&id={public_id}
Disclaim your own claim, optionally with {"reason": "β¦"}.
Returns 403 for a claim you do not own.
The record is preserved and annotated as disclaimed. Claims are never deleted β that is what makes the registry worth trusting, and it is why this is not a DELETE.
?action=supersede
Link a claim as the next revision of an earlier one.
{ "claim": "def456uvw", "supersedes": "abc123xyz" }
You must own both claims, or anyone could graft their work
onto a stranger's lineage. Returns 422 if the link is not
permitted β not the owner of both, already linked, or would create a cycle.
Neither timestamp changes, and the link cannot be edited afterwards.
?action=attest&id={public_id}
Confirm or dispute someone else's claim.
{ "stance": "confirms", "statement": "I was present when this was said." }
stance is confirms or disputes.
Self-attestation is refused (422) β a claim is already the
owner's assertion, so co-signing it would manufacture corroboration from
nothing. A verified email is also required. 60 per hour per account.
Webhooks
Configure a webhook URL from your Profile page. When an event fires, we POST a signed JSON payload to your URL.
Verifying signatures
Each request includes an X-KOT-Signature header:
X-KOT-Signature: sha256=HEX
Verify it in PHP:
$payload = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_KOT_SIGNATURE'] ?? '';
$expect = 'sha256=' . hash_hmac('sha256', $payload, YOUR_WEBHOOK_SECRET);
if (!hash_equals($expect, $sig)) {
http_response_code(401);
exit;
}
Or in Python:
import hmac, hashlib
def verify(payload: bytes, secret: str, header: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
Events
claim.created
Fires when you register a new claim. Example payload:
{
"event": "claim.created",
"claim": {
"public_id": "abc123xyz",
"title": "My original essay",
"content_type": "text",
"content_hash": "e3b0c44298fc1c149afb4c8996fb92427ae41e4649b934ca495991b7852b855",
"claimed_at": "2025-06-01T14:32:00Z",
"verify_url": "https://kerneloftruth.org/verify/abc123xyz"
}
}
attestation.added
Another account confirmed or disputed one of your claims. Check
stance β it is confirms or disputes.
{
"event": "attestation.added",
"claim_id": 1234,
"stance": "confirms"
}
dispute.filed
Someone opened a formal dispute against one of your claims. The record is public and you are expected to be able to respond, so this also sends email unless you turn it off in your profile.
{
"event": "dispute.filed",
"claim_id": 1234,
"dispute_id": 12,
"record_url": "https://kerneloftruth.org/dispute/12"
}
ots.upgraded
A claim of yours became anchored to Bitcoin β the OpenTimestamps
commitment matured into a full attestation. The
.ots file at proof_url can now be verified
offline with the ots client.
{
"event": "ots.upgraded",
"claim_id": "abc123xyz",
"block_height": 925017,
"proof_url": "https://kerneloftruth.org/ots.php?id=abc123xyz"
}
Command line and CI
Both tools hash content locally and send only the SHA-256 β the same promise this site makes. Neither has any third-party dependency.
CLI
curl -o /usr/local/bin/kot https://kerneloftruth.org/cli/kot chmod +x /usr/local/bin/kot export KOT_API_KEY=kot_xxxxxxxx kot claim manuscript.pdf --title "Chapter 1 draft" --stamp kot check manuscript.pdf # exit 0 registered, 2 not kot verify abc123xyz # check the Bitcoin anchor kot receipt manuscript.pdf -o receipt.json kot list kot transparency
check's exit code composes in scripts:
kot check f || kot claim f --stamp. Read-only commands need
no API key.
GitHub Action
- uses: actions/checkout@v4
- run: make dist
- uses: karmstrong/wholecob@main
with:
api-key: ${{ secrets.KOT_API_KEY }}
files: dist/*.tar.gz
Or claim a whole commit tree β one hash covering every tracked file:
- uses: karmstrong/wholecob@main
with:
api-key: ${{ secrets.KOT_API_KEY }}
mode: commit
mode: commit hashes git ls-tree -r HEAD, which
already commits to every tracked blob. Deliberately not the commit SHA β
that is a git-internal identifier meaning nothing to a verifier without
your repository. Anyone with a checkout can reproduce it:
git ls-tree -r HEAD | shasum -a 256.
Rate limits
Unauthenticated requests: 120 per minute per IP address.
Authenticated requests: 300 per minute per API key.
Exceeding the limit returns 429 Too Many Requests with a Retry-After: 60 header.