Kernsignal/API docs

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:

What it does not say. It does not say the panel is accurate, validated or certified, and it does not endorse a vendor. The limits of the method are part of the signed record.

Endpoints

Method and pathAuthWhat it does
GET /v1/audits/{id}/verificationkeyThe record for a succeeded audit. Issued on first request, the same one afterwards.
POST /v1/audits/{id}/verificationkeyBody {"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/revokekeyBody {"reason": "..."}. See Revoke.
GET /v1/verify/{verification_id}noneThe public record. Browsers get the page; other clients get JSON. Add ?format=json or ?format=html to choose.
GET /v1/verify/{verification_id}/badge.svgnoneThe badge image.
GET /v1/verification-keysnoneThe 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.