Skip to content
Talk to our solutions team

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).

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 return 401 invalid attempt detected by user. If the tenant doesn’t exist in the platform config, the request is rejected with 400 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.

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.

{
"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-..."
}
FieldTypeRequiredNotes
log_idstringyesLog namespace within the tenant. Allowed chars: [a-zA-Z0-9/._:-], max 256 bytes, no .. traversal.
payloadbase64 stringyesOpaque bytes, ≤ 16 MiB. Hashed to form the leaf.
actor_idstringnoFree-form identity of the entity producing the event. Recorded verbatim.
actor_signaturebase64 stringnoOptional signature over payload by the actor’s key. Stored verbatim; not verified by the service.
actor_key_idstringnoIdentifier the verifier needs to find the actor’s public key.
metadatamap<string,string>noIndexable side-channel; ≤ 64 KiB total.
dedup_keystringnoCaller-supplied idempotency key. (Reserved — full dedup semantics still pending in lib-audit.)
{
"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).

{
"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.

{
"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).

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).

Terminal window
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"}
}
}'

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).

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.

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.

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.

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.

Liveness and readiness probes provided by svcboot. Both return 200 ok. No auth required.

All error responses are JSON of the form:

{ "code": "invalid_argument", "message": "submission.payload is empty" }
CodeHTTP statusMeaning
unauthenticated401Missing/bad JWT, or no X-Tenant.
permission_denied403JWT valid but caller not allowed for this resource. (Reserved — current build only emits this if a future authz layer is added.)
not_found404Referenced log can’t be opened (missing routing rule, etc.).
invalid_argument400Malformed body, bad field values, out-of-range proof request.
internal500Disk error, signer failure, vault unreachable.
  • Request bodies are capped at 32 MiB. Larger payloads return 400.
  • Individual Submission.payload is capped at 16 MiB by lib-audit.
  • metadata total size is capped at 64 KiB.
  • /read streams indefinitely; clients control bounds via from_seq / to_seq and may close the connection at any time.
  • Tenant comes from X-Tenant and 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 /logs filters to the caller.
  • Within a tenant, log_id is a free namespace. There is no pre-registration; logs are created on first /submit.

A consumer who wants to independently verify an entry should:

  1. Call /inclusion-proof for (sequence, tree_size).
  2. Call /head and confirm the returned tree_size is ≥ what was used in step 1; cross-check root_hash.
  3. Reconstruct the leaf hash from the original payload (SHA-256 of the canonical leaf encoding — see lib-audit/segment for the format).
  4. Walk the proof hashes upward to the root and compare to step 2.
  5. Verify the head’s signature against the tenant’s public key (the public half of the key at signer.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.