Verification
Verification
A Kernsignal Verified record is a signed statement that an audit was run, on which files, and what it reported. Anyone can check it without trusting the person who shows it.
What a record asserts
Exactly this, and nothing more:
- A Kernsignal Synthetic Panel Audit was run on input files with these SHA-256 hashes (the synthetic file and the human file).
- It scored this many questions, put this many in each bin, and says how many rest on human answers.
- It ran under this ranker version and method version, with this TVD threshold and minimum cell size.
- It was issued at this time. If the account holder opted in, it carries the organisation name they supplied. Kernsignal does not check that name.
Endpoints
| Method and path | Auth | What it does |
|---|---|---|
GET /v1/audits/{id}/verification | key | The record for a succeeded audit. Issued on first request, the same one afterwards. |
POST /v1/audits/{id}/verification | key | Body {"display_name": "Acme Research"} shows your organisation name; null hides it. The record is re-signed with a higher revision. |
POST /v1/audits/{id}/verification/revoke | key | Body {"reason": "..."}. See Revoke. |
GET /v1/verify/{verification_id} | none | The public record. Browsers get the page; other clients get JSON. Add ?format=json or ?format=html to choose. |
GET /v1/verify/{verification_id}/badge.svg | none | The badge image. |
GET /v1/verification-keys | none | The public keys, same content as the published key file. |
Only an audit that has succeeded can be verified, and only if its input hashes were recorded when it was created. An audit also needs human evidence: if no human_file was uploaded, or no question was scored against human answers, GET /v1/audits/{id}/verification returns 400 verification_requires_human_evidence and no record or badge is created. A record that says how many questions were ranked without any human data would say more than the audit did.
The JSON record
{
"id": "ver_...",
"object": "verification",
"status": "valid",
"signature_valid": true,
"mode": "live",
"payload": {
"schema": "kernsignal.verification.v1",
"verification_id": "ver_...",
"revision": 1,
"issuer": "Kernsignal",
"issued_at": "2026-11-14T10:05:00Z",
"mode": "live",
"org_display_name": null,
"statement": "...",
"audit": {
"id": "aud_...",
"finished_at": "2026-11-14T10:00:40Z",
"inputs": {
"synthetic": {"sha256": "...", "bytes": 62818},
"human": {"sha256": "...", "bytes": 61972}
},
"questions": {"scored": 20, "not_scored": 6, "with_human_evidence": 20},
"bins": {"use": 3, "spot_check": 12, "field": 5},
"bins_by_evidence": {"use": {"observed": 3, "ranker": 0}, "spot_check": {"observed": 12, "ranker": 0}, "field": {"observed": 5, "ranker": 0}},
"ranker_version": "twin2k-f883165a",
"method_version": "kernsignal-audit/0.1.0",
"tvd_threshold_permille": 150,
"min_cell_n": 50,
"subgroup_variables": 1
},
"limits": "..."
},
"signature": {"alg": "Ed25519", "key_id": "ksv-2026-11", "value": "..."},
"signed_payload": "...",
"keys_url": "https://kernsignal.ai/.well-known/kernsignal-verification-keys.json"
}
The numbers above are an illustration. status is valid, revoked or invalid (the server could not verify its own stored signature). Rates are whole numbers (tvd_threshold_permille: 150 means 0.150) because signed payloads contain no floating point.
Badge
The response of GET /v1/audits/{id}/verification includes badge_html, ready to paste:
<a href="https://api.kernsignal.ai/v1/verify/ver_..." rel="noopener"><img src="https://api.kernsignal.ai/v1/verify/ver_.../badge.svg" alt="Kernsignal Verified: audit on record" height="24"></a>
The badge reads "audit on record" and links to the public page. It turns to "record revoked" if you revoke the record, and says "sandbox audit" for a test-mode record. Wording rule: place the badge next to a sentence such as "An audit of this panel is on record", and do not add words like accurate, validated or certified. The record does not support them.
Check the signature yourself
Kernsignal signs with Ed25519. The signed message is the ASCII line kernsignal.verification.v1 and a newline, followed by the canonical JSON of payload: UTF-8, object keys sorted, no whitespace, no floating point numbers. Public keys are published at kernsignal.ai/.well-known/kernsignal-verification-keys.json; pick the one whose key_id matches signature.key_id. Keys and signatures are base64url without padding.
Python
import base64, json, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def b64u(text):
return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
verify_url = "https://api.kernsignal.ai/v1/verify/ver_..."
record = json.load(urllib.request.urlopen(urllib.request.Request(verify_url, headers={"Accept": "application/json"})))
keys = json.load(urllib.request.urlopen("https://kernsignal.ai/.well-known/kernsignal-verification-keys.json"))
key = next(k for k in keys["keys"] if k["key_id"] == record["signature"]["key_id"])
Ed25519PublicKey.from_public_bytes(b64u(key["public_key"])).verify(
b64u(record["signature"]["value"]),
b"kernsignal.verification.v1\n" + canonical(record["payload"]).encode("utf-8"),
)
print("signature valid") # verify() raises InvalidSignature otherwise
Node.js
verifyRecordSignature(record, keysDocument) in kernsignal.mjs does the same with node:crypto and no dependencies.
A valid signature proves the record is what Kernsignal issued. The record's status can still change: check GET /v1/verify/{id} for revocation.
Revoke
Revoke a record if the claim should no longer be relied on, for example because the panel was replaced. POST /v1/audits/{id}/verification/revoke with {"reason": "Panel replaced"} (1 to 200 characters; the reason is shown on the public page). The badge and page then say revoked. A revoked record cannot be edited. Revoking is final and stays after the audit result is deleted.
Keys and rotation
Each record names the key that signed it. When the signing key is replaced, the old public key stays in the published file with "status": "retired", so older records still verify. Never hard-code a single key; read the file.