Skip to content
Talk to our solutions team

Audit Quickstart

Audience: application developers wiring audit calls into your service. You’re a consumer of the audit HTTP API; you do NOT need the audit.svc source. Five minutes from “I have a JWT” to “my record is signed into a tamper-evident log.”

For the comprehensive endpoint reference, see the HTTP API. This page gets you to your first authenticated call as fast as possible; the reference is your daily-driver doc after that.

  • A reachable audit.svc instance (your platform team will give you the URL; in dev that’s typically https://audit.dev.kis.ai).
  • A JWT bearer token issued by the kis.ai IAM service for an identity in your tenant, the token’s tenant claim must match the X-Tenant header you send.
  • Your tenant id (the same one your other kis.ai services use).
  • A vault entry for your tenant’s Ed25519 signing key already provisioned by ops at the configured vault_path_template (e.g. audit/<your-tenant>/ed25519-private-key). If it’s missing, your first /submit will return 500: vault signer: get secret …. Ask ops to provision before continuing.

Two identifiers fully address a log:

  • tenant: comes from X-Tenant header on every request. Chassis cross-checks it against your JWT’s tenant claim.
  • log_id: a free namespace within your tenant. You pick the names: auth, billing, admin-actions, whatever fits your product. No pre-registration; logs are created on first /submit.

You append opaque bytes (payload) plus structured metadata, the service hashes everything, assigns a sequence, and gives you back a Receipt. Periodically the service signs the Merkle root of everything appended so far. That signed TreeHead plus an inclusion proof lets anyone verify your record is in the log and the log hasn’t been tampered with.

Terminal window
export AUDIT_URL=https://audit.dev.kis.ai
export JWT=eyJhbGciOi... # from IAM
export TENANT=cust-acme
curl -sX POST $AUDIT_URL/submit \
-H "Authorization: Bearer $JWT" \
-H "X-Tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{
"submission": {
"log_id": "auth",
"payload": "'"$(echo -n '{"event":"login.success","user":"alice"}' | base64)"'",
"actor_id": "user:alice",
"metadata": {"event_type": "auth.login.success"}
}
}' | jq

Expected response:

{
"receipt": {
"log_id": "auth",
"sequence": 0,
"leaf_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"timestamp": "2026-05-13T14:00:00.123456789Z"
}
}

sequence: 0 means this is the first leaf in your auth log. The log was created on the call.

Force a fresh signed tree head, then ask for an inclusion proof:

Terminal window
# Seal — returns the latest signed root.
curl -sX POST $AUDIT_URL/seal \
-H "Authorization: Bearer $JWT" -H "X-Tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{"log_id": "auth"}' | jq
# Inclusion proof for sequence 0 against tree_size 1.
curl -sX POST $AUDIT_URL/inclusion-proof \
-H "Authorization: Bearer $JWT" -H "X-Tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{"log_id": "auth", "sequence": 0, "tree_size": 1}' | jq

The full client-side verification walk (hash chain → proof path → root → Ed25519 signature) is in the HTTP API under “Verification.”

NDJSON, one Leaf per line. to_seq: 0 means “to the current end.”

Terminal window
curl -sX POST $AUDIT_URL/read \
-H "Authorization: Bearer $JWT" -H "X-Tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{"log_id": "auth", "from_seq": 0, "to_seq": 0, "with_body": true}'

If your stream truncates mid-read (network blip), resume by re-requesting with from_seq set to one past the last sequence you saw. There’s no SSE-style auto-resume.

package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
)
type submission struct {
LogID string `json:"log_id"`
Payload []byte `json:"payload"`
ActorID string `json:"actor_id,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
func main() {
body, _ := json.Marshal(map[string]submission{
"submission": {
LogID: "auth",
Payload: []byte(`{"event":"login.success","user":"alice"}`),
ActorID: "user:alice",
Metadata: map[string]string{"event_type": "auth.login.success"},
},
})
req, _ := http.NewRequest("POST",
os.Getenv("AUDIT_URL")+"/submit", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("JWT"))
req.Header.Set("X-Tenant", os.Getenv("TENANT"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out map[string]map[string]any
_ = json.NewDecoder(resp.Body).Decode(&out)
fmt.Printf("sequence=%v leaf_hash=%v\n",
out["receipt"]["sequence"], out["receipt"]["leaf_hash"])
_ = base64.StdEncoding // imported for symmetry with payload encoding
}

net/http handles base64 encoding of []byte fields automatically when marshaling JSON. No manual base64.StdEncoding.EncodeToString needed for Payload.

HTTPcodeRetry?What to do
200——Persist the receipt.sequence if you need to prove later.
400invalid_argumentNoYour request is malformed (missing payload, bad log_id chars, etc.). Fix the body.
401unauthenticatedNoRefresh JWT or fix X-Tenant. Chassis rejects mismatches between JWT claim and header with this code too.
404not_foundNoThe log_id doesn’t match any routing rule: ops needs to add one.
500internalYes, with backoffDisk error, vault unreachable, signer failure. Workers behind the scenes back off and retry; you should too.
  • payload is opaque, capped at 16 MiB. The service hashes it but never inspects. Compress or split your own large records before submitting.
  • metadata capped at 64 KiB total. Keep it small: high-cardinality fields belong in the payload.
  • dedup_key is reserved. audit.svc hasn’t shipped dedup semantics yet; do not rely on it to suppress duplicates.
  • /logs returns only logs OPEN IN MEMORY on the responding process. A multi-replica deployment will show different lists on different hits. It’s a debug surface, not a tenant inventory.
  • No actor_signature verification. The service stores actor_signature / actor_key_id verbatim. Downstream verifiers do the actor-cert check; the service only signs the Merkle root.
  • Streaming /read cannot signal mid-stream errors. A truncated response is a failure; resume from the last seen sequence.
  • Per-log single-writer. audit.svc enforces this with a POSIX file lock. In a multi-replica deployment, the same (tenant, log_id) should always route to the same replica (your platform team handles this: but if you see “log is locked” errors, that’s the affinity story breaking down).
  • HTTP API: full endpoint reference, every field documented.
  • The OpenAPI spec: import into Bruno / Postman / Insomnia for canned, typed requests.
  • End-to-end testing: write acceptance tests against your dev instance.