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.

GET ?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
}
GET ?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"
}
GET ?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"
    }
  ]
}
POST ?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.

GET ?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"
    }
  ]
}
POST ?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.

POST ?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.

POST ?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.

GET ?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.

GET ?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.

GET /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"
    }
  ]
}
GET /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:

  1. Fetch every active claim in the root's from_claim_idto_claim_id range, ordered by claim ID ascending.
  2. 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)
  3. Combine adjacent pairs upward, prefixing internal nodes with node::
    node_digest = SHA256("node:" + left_digest + right_digest)
  4. If a level has an odd number of nodes, promote the last one unchanged to the next level. Do not duplicate it.
  5. 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_...

GET ?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
}
POST ?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"
}
POST ?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.

POST ?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.

POST ?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.

POST ?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.

POST ?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

EVENT 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"
  }
}
EVENT 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"
}
EVENT 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"
}
EVENT 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.