Audit API
Reference for the audit.svc HTTP surface. For concepts see the Audit block docs.
This is the wire reference for callers. For the internal architecture and outstanding work, see the design specs.
All endpoints are JSON-over-HTTPS, all POST (except health probes), and
all rooted at the service host (no /v1 prefix).
Authentication
Section titled “Authentication”Every request must carry both:
Authorization: Bearer <jwt>— validated by the chassis JWT middleware (httputil.SecureWithJWTToken). The token is rejected if signature/expiry are invalid.X-Tenant: <tenant-id>— must match the tenant claim inside the JWT. The chassis enforces this cross-check; mismatches return401 invalid attempt detected by user. If the tenant doesn’t exist in the platform config, the request is rejected with400 invalid tenant.
Service-to-service tokens issued via the certificate flow
(ClaimsFlowTypeCertificate) and superadmin-flagged callers
(X-SuperAdmin: true) bypass the tenant cross-check. Treat
X-SuperAdmin as platform-internal only.
The (tenant, log_id) pair fully identifies a log. The tenant is
always taken from the header — never from the request body — and
data lives at <base_dir>/<tenant>/<log_id>/ on disk.
Common types
Section titled “Common types”These types appear in multiple endpoints. Field-level encoding notes
matter: hashes are hex, signatures and binary payloads are base64
(stdlib base64.StdEncoding), timestamps are RFC3339 with nanoseconds.
Submission (request input to /submit)
Section titled “Submission (request input to /submit)”{ "log_id": "auth", "payload": "eyJldmVudCI6ImxvZ2luIn0=", "actor_id": "user:alice", "actor_signature": "MEUCIQ...", "actor_key_id": "ak-2026-05", "metadata": {"event_type": "auth.login.success"}, "dedup_key": "evt-7f3a-..."}| Field | Type | Required | Notes |
|---|---|---|---|
log_id | string | yes | Log namespace within the tenant. Allowed chars: [a-zA-Z0-9/._:-], max 256 bytes, no .. traversal. |
payload | base64 string | yes | Opaque bytes, ≤ 16 MiB. Hashed to form the leaf. |
actor_id | string | no | Free-form identity of the entity producing the event. Recorded verbatim. |
actor_signature | base64 string | no | Optional signature over payload by the actor’s key. Stored verbatim; not verified by the service. |
actor_key_id | string | no | Identifier the verifier needs to find the actor’s public key. |
metadata | map<string,string> | no | Indexable side-channel; ≤ 64 KiB total. |
dedup_key | string | no | Caller-supplied idempotency key. (Reserved — full dedup semantics still pending in lib-audit.) |
Receipt (returned by /submit)
Section titled “Receipt (returned by /submit)”{ "log_id": "auth", "sequence": 42, "leaf_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "timestamp": "2026-05-13T14:00:00.123456789Z"}sequence is the position in the log (0-indexed). leaf_hash is the
SHA-256 of the canonical leaf encoding (hex).
TreeHead (returned by /head, /seal)
Section titled “TreeHead (returned by /head, /seal)”{ "tree_size": 1024, "root_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "timestamp": "2026-05-13T14:01:00Z", "key_id": "0a1b2c3d4e5f6071829304056789abcd", "signature": "MEUCIQDx..."}tree_size is the number of leaves committed under this head.
root_hash is the RFC 6962 Merkle root (hex). signature is the
Ed25519 signature over the canonical head encoding (base64).
key_id is the deterministic 16-byte identifier of the signing key.
Leaf (streamed by /read)
Section titled “Leaf (streamed by /read)”{ "sequence": 42, "timestamp": "2026-05-13T14:00:00Z", "prev_hash": "0000...", "payload_hash": "9f86...", "leaf_hash": "ab12...", "metadata": {"event_type": "auth.login.success"}, "payload": "eyJldmVudCI6ImxvZ2luIn0=", "actor_key_id": "ak-2026-05", "actor_signature": "MEUC..."}payload is included only when the request set with_body: true.
prev_hash chains backward to the previous leaf (zeroes for the first).
Endpoints
Section titled “Endpoints”POST /submit
Section titled “POST /submit”Append a record to (tenant, log_id). Creates the log on first call.
Request body:
{ "submission": {SUBMISSION} }Response:
{ "receipt": {RECEIPT} }200 on success. Errors: 400 (missing payload, invalid log_id),
401 (auth), 500 (signer failure, disk error).
curl -X POST https://audit.kis.ai/submit \ -H "Authorization: Bearer $JWT" \ -H "X-Tenant: cust-acme" \ -H "Content-Type: application/json" \ -d '{ "submission": { "log_id": "auth", "payload": "eyJldmVudCI6ImxvZ2luIn0=", "actor_id": "user:alice", "metadata": {"event_type": "auth.login.success"} } }'POST /head
Section titled “POST /head”Return the most recently signed tree head for (tenant, log_id).
Request:
{ "log_id": "auth" }Response:
{ "head": {TREE_HEAD} }head is null if the log exists but has never been sealed (no
appends or no /seal call yet).
POST /inclusion-proof
Section titled “POST /inclusion-proof”Return an RFC 6962 inclusion proof for a leaf at sequence against
the tree of size tree_size.
Request:
{ "log_id": "auth", "sequence": 42, "tree_size": 1024}Response:
{ "leaf_index": 42, "tree_size": 1024, "hashes": [ "ab12...", "cd34..." ]}Each hash is hex-encoded SHA-256. Verify against the root_hash
returned by /head for the same tree_size.
Errors: 400 if sequence >= tree_size or tree_size exceeds the
current log size.
POST /read
Section titled “POST /read”Stream leaves in the half-open range [from_seq, to_seq). Response is
newline-delimited JSON (Content-Type: application/x-ndjson),
one Leaf per line. to_seq: 0 means “to the current end”.
Request:
{ "log_id": "auth", "from_seq": 0, "to_seq": 100, "with_body": true}Response:
{"sequence":0,"timestamp":"2026-05-13T...","prev_hash":"...","leaf_hash":"...","payload":"..."}{"sequence":1,"timestamp":"2026-05-13T...","prev_hash":"...","leaf_hash":"...","payload":"..."}...The stream may terminate early if the client disconnects. Errors that
occur after the response has started cannot be signalled in JSON;
clients should treat a truncated stream as an error and resume from
the last sequence they saw.
When with_body is false the payload field is omitted — useful
for cheap integrity scans.
POST /seal
Section titled “POST /seal”Force a fresh signed tree head for (tenant, log_id). Normally the
sealer worker handles this on a cadence; call this explicitly when you
need a fresh root now (e.g., before publishing an inclusion proof to
an external party).
Request:
{ "log_id": "auth" }Response:
{ "head": {TREE_HEAD} }Errors: 400 log is empty; nothing to seal if the log has never been
appended to.
POST /logs
Section titled “POST /logs”List the logs that the calling tenant has open in this service
process. Empty body or {}.
Request:
{}Response:
{ "logs": [ { "log_id": "auth", "tree_size": 1024 }, { "log_id": "billing", "tree_size": 7 } ]}Note: only logs that are currently open in memory on the responding process are returned. Logs that exist on disk but haven’t been touched since startup won’t appear until the first request opens them.
GET /health, GET /ready
Section titled “GET /health, GET /ready”Liveness and readiness probes provided by svcboot. Both return
200 ok. No auth required.
Errors
Section titled “Errors”All error responses are JSON of the form:
{ "code": "invalid_argument", "message": "submission.payload is empty" }| Code | HTTP status | Meaning |
|---|---|---|
unauthenticated | 401 | Missing/bad JWT, or no X-Tenant. |
permission_denied | 403 | JWT valid but caller not allowed for this resource. (Reserved — current build only emits this if a future authz layer is added.) |
not_found | 404 | Referenced log can’t be opened (missing routing rule, etc.). |
invalid_argument | 400 | Malformed body, bad field values, out-of-range proof request. |
internal | 500 | Disk error, signer failure, vault unreachable. |
Streaming and request size limits
Section titled “Streaming and request size limits”- Request bodies are capped at 32 MiB. Larger payloads return
400. - Individual
Submission.payloadis capped at 16 MiB by lib-audit. metadatatotal size is capped at 64 KiB./readstreams indefinitely; clients control bounds viafrom_seq/to_seqand may close the connection at any time.
Multi-tenancy
Section titled “Multi-tenancy”- Tenant comes from
X-Tenantand is cross-checked against the JWT tenant claim by the chassis. - A tenant cannot read or write another tenant’s logs — the
filesystem path includes the tenant, signing keys are per-tenant,
and
/logsfilters to the caller. - Within a tenant,
log_idis a free namespace. There is no pre-registration; logs are created on first/submit.
Verification
Section titled “Verification”A consumer who wants to independently verify an entry should:
- Call
/inclusion-prooffor(sequence, tree_size). - Call
/headand confirm the returnedtree_sizeis ≥ what was used in step 1; cross-checkroot_hash. - Reconstruct the leaf hash from the original
payload(SHA-256 of the canonical leaf encoding — seelib-audit/segmentfor the format). - Walk the proof hashes upward to the root and compare to step 2.
- Verify the head’s
signatureagainst the tenant’s public key (the public half of the key atsigner.vault_path_template).
Step 5 implies callers need access to the tenant’s public key out of
band. A future endpoint to expose /pubkey is planned.