KT Registry Endpoints (v0.1)
Implementer reference for the LLMO Key Transparency registry HTTP API specified in LIP-4 §3.3.
Implementer-facing API reference for the LLMO Key Transparency (KT) registry. The protocol-level specification is in LIP-4; this page is the concrete wire format and behavior contract that conforming registry implementations and consumers MUST agree on.
The reference operator is Diverse.org, hosting the canonical v0.1.x registry at https://llmo.org/kt/v1/. Future operators participating in v0.2 federation MUST serve the same wire format under their own base URL.
Base path
All endpoints are served under /kt/v1/. The version segment (v1) reflects this LIP’s API revision, not the LLMO spec version; a future LIP introducing a non-backward-compatible API change would increment to /kt/v2/.
All responses include CORS header Access-Control-Allow-Origin: *. The registry surfaces are public.
Endpoints
POST /kt/v1/entries
Submit a new KT entry.
Request:
POST /kt/v1/entries HTTP/1.1
Host: llmo.org
Content-Type: application/jose+json
<compact-JWS-string>
The request body is a compact-serialization JWS per RFC 7515, with the protected header and payload as specified in LIP-4 §3.2.
Validation pipeline (registry MUST perform all in order):
- Body MUST be a syntactically valid compact JWS (three base64url segments separated by
.). - Protected header MUST be a JSON object containing at minimum
alg,kid,typ, andjwk. algMUST be one of the algorithms permitted by spec §4.2 (ES256,ES384,EdDSA).typMUST equalllmo-kt-entry+jws.jwkMUST be a JSON Web Key containing only public-key parameters for the key type. Private-key parameters (dfor EC keys, equivalent for other types) MUST be absent. Registry rejects with400 jwk_contains_private_materialif present.- Payload MUST be a JSON object containing
domain,kid,jwk_thumbprint,doc_url,doc_id,observed_at. Other fields are allowed and preserved verbatim. payload.kidMUST equalprotected.kid. Registry rejects with400 kid_mismatchif not.payload.jwk_thumbprintMUST equalbase64url(SHA-384(JCS(protected.jwk)))per LIP-4 §3.1. Registry rejects with400 thumbprint_mismatchif not.- JWS signature MUST verify against
protected.jwkusingprotected.alg. Registry rejects with400 signature_invalidif not. payload.domainMUST be a valid public hostname (RFC 1035 syntax, no IP literals, contains at least one dot). Registry rejects with400 invalid_domainotherwise.payload.observed_atMUST be a valid RFC 3339 timestamp within ±5 minutes of the registry’s clock at receipt time. Registry rejects with400 timestamp_out_of_rangeotherwise. This bound prevents both backdated entries and unbounded clock drift on the publisher side.payload.doc_urlMUST be ahttps://URL whose hostname equalspayload.domainfollowed by/.well-known/llmo.json. Registry rejects with400 doc_url_mismatchotherwise.- Submitter source IP MUST be within rate limits (default 100 entries per hour per IP). Registry rejects with
429 rate_limitedotherwise.
If all checks pass, the registry assigns an entry_id (auto-incrementing integer starting at 1) and a log_position (equal to entry_id in single-operator v0.1.x), appends to the log, and returns:
Success response:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /kt/v1/entries/<entry_id>
{
"entry_id": 1234,
"log_position": 1234,
"appended_at": "2026-05-12T15:38:11Z",
"receipt": "<compact-JWS-string>"
}
The receipt is a JWS signed by the registry’s signing key with payload {entry_id, log_position, appended_at, entry_jws_hash}, where entry_jws_hash = base64url(SHA-384(<compact-JWS-string>)). The receipt is the registry’s signed attestation that the submitted entry was accepted at the given position.
Error response:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "<error_code>",
"detail": "<human-readable explanation>"
}
Defined error codes: malformed_jws, missing_protected_field, unsupported_alg, wrong_typ, jwk_contains_private_material, missing_payload_field, kid_mismatch, thumbprint_mismatch, signature_invalid, invalid_domain, timestamp_out_of_range, doc_url_mismatch, rate_limited.
GET /kt/v1/entries
Query entries for a given domain.
Request:
GET /kt/v1/entries?domain=<primary_domain>&limit=<n> HTTP/1.1
Query parameters:
domain(required): the publisher’sprimary_domain. Lowercased before matching.limit(optional, default 10, max 100): maximum number of entries returned.
Response:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
{
"domain": "example.com",
"entries": [
{
"entry_id": 1234,
"log_position": 1234,
"entry": "<compact-JWS-string>",
"appended_at": "2026-05-12T15:38:11Z"
}
],
"total": 1
}
Entries are returned in descending log_position order (most recent first). The entry field is the original compact JWS submitted by the publisher; consumers re-verify against the inline jwk per LIP-4 §3.6.
If no entries exist for the domain: entries: [], total: 0, status 200 OK (not 404, to distinguish from registry errors).
GET /kt/v1/entries/{entry_id}
Fetch a single entry by its log index.
Response:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=3600
{
"entry_id": 1234,
"log_position": 1234,
"entry": "<compact-JWS-string>",
"appended_at": "2026-05-12T15:38:11Z"
}
404 Not Found if entry_id does not exist or has been removed (entries are append-only and never removed; 404 indicates the ID was never assigned).
GET /kt/v1/log.jsonl
Bulk download of the full append-only log.
Response:
HTTP/1.1 200 OK
Content-Type: application/x-ndjson
Cache-Control: max-age=300
<compact-JWS-of-entry-1>
<compact-JWS-of-entry-2>
...
<compact-JWS-of-entry-N>
One entry per line, in ascending log_position order. Each line is the original compact JWS submitted by the publisher. The bulk-download surface is intended for consumers who want to verify the registry independently or maintain a local mirror.
A consumer who has retained a prior snapshot may verify integrity by re-computing SHA-384(<bytes-of-log.jsonl-prefix-up-to-snapshot-time>) and comparing to the snapshot’s log_hash. The flat log content at any prefix-time MUST match the corresponding snapshot.
GET /kt/v1/snapshot/latest
Fetch the most recent signed snapshot of the log.
Response:
HTTP/1.1 200 OK
Content-Type: application/jose+json
Cache-Control: max-age=300
<compact-JWS-of-snapshot>
The JWS is signed by the registry’s signing key. Decoded payload:
{
"snapshot_id": 1234,
"log_size": 5678,
"log_hash": "<base64url(SHA-384(log.jsonl-bytes-at-snapshot-time))>",
"snapshot_at": "2026-05-12T02:00:00Z",
"previous_snapshot_id": 1233,
"previous_log_hash": "<base64url hash from the prior snapshot>"
}
For the very first snapshot: previous_snapshot_id and previous_log_hash are null.
GET /kt/v1/snapshot/{snapshot_id}
Fetch a historical snapshot by ID. Response format identical to /snapshot/latest. 404 Not Found if the ID does not exist.
D1 schema (reference)
The reference registry uses Cloudflare D1 (serverless SQLite) for the dynamic query surfaces. The schema is:
CREATE TABLE entries (
entry_id INTEGER PRIMARY KEY AUTOINCREMENT,
log_position INTEGER NOT NULL UNIQUE,
domain TEXT NOT NULL,
kid TEXT NOT NULL,
jwk_thumbprint TEXT NOT NULL,
doc_url TEXT NOT NULL,
doc_id TEXT NOT NULL,
observed_at TEXT NOT NULL,
appended_at TEXT NOT NULL,
entry_jws TEXT NOT NULL,
source_ip TEXT NOT NULL
);
CREATE INDEX idx_entries_domain ON entries(domain);
CREATE INDEX idx_entries_thumbprint ON entries(jwk_thumbprint);
CREATE INDEX idx_entries_appended ON entries(appended_at);
CREATE TABLE rate_limits (
source_ip TEXT NOT NULL,
window_start TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (source_ip, window_start)
);
D1 is a query accelerator for the dynamic surfaces (/entries, /entries/{id}). The canonical record is the JSONL log file at static/kt/v1/log.jsonl, written by a scheduled flush from D1.
KV namespace (reference)
The reference registry uses Cloudflare Workers KV for snapshot storage. Keys:
snapshot:latest→ the most recent signed snapshot JWS.snapshot:<snapshot_id>→ a specific snapshot JWS by ID.signing-key-meta→ JSON metadata about the registry’s current signing key (kid, created_at, public JWK fingerprint).
The actual signing key (private material) lives in Workers Secrets, not in KV. KV stores only public-readable artifacts.
Algorithms
- JWK Thumbprint: RFC 7638 with SHA-384.
- Snapshot signing: ES384 (reference). Spec permits any algorithm in spec §4.2.
- Snapshot signing cadence: every 24 hours at 02:00 UTC.
Compliance test vectors
To be published at /spec/v0.1/test-vectors/kt/ before LIP-4 transitions to Final. Vectors will cover:
- Canonical entry construction (publisher-side): given a kid + private key + payload fields, produce the entry JWS.
- Registry validation (positive): a syntactically valid entry passes all 13 validation steps.
- Registry validation (negative): one negative vector per defined error code.
- Snapshot construction: given a log content and previous snapshot, produce the next snapshot JWS.
- Consumer X7 evaluation: given a registry response and a fetched llmo.json, evaluate X7 PASS / FAIL.
Federation considerations (v0.2)
This API is designed to be implementable by multiple independent operators. A future v0.2 LIP will introduce cross-witness signing: each operator periodically signs the others’ snapshot roots, and the consumer-side X7 check requires inclusion in at least N of M conforming logs. The wire format above does not change for federation; the consumer’s policy about which registries to query changes.
Operators participating in v0.2 federation MUST:
- Serve this API at a stable base URL.
- Sign their own snapshots with their own ES384 (or other §4.2-permitted) key, publishing the public JWK at a stable JWKS endpoint.
- Periodically cross-sign the most recent snapshot of every other federated operator. The cross-signing format and cadence will be defined in the v0.2 LIP.
For v0.1.x, federation is roadmap, not requirement. The Diverse.org-operated registry at https://llmo.org/kt/v1/ is the canonical and only conforming registry.