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.
1. Prerequisites
Section titled “1. Prerequisites”- A reachable
audit.svcinstance (your platform team will give you the URL; in dev that’s typicallyhttps://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-Tenantheader 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/submitwill return500: vault signer: get secret …. Ask ops to provision before continuing.
2. Mental model
Section titled “2. Mental model”Two identifiers fully address a log:
tenant— comes fromX-Tenantheader 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.
3. Submit your first record
Section titled “3. Submit your first record”export AUDIT_URL=https://audit.dev.kis.aiexport JWT=eyJhbGciOi... # from IAMexport 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"} } }' | jqExpected 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.
4. Verify it landed
Section titled “4. Verify it landed”Force a fresh signed tree head, then ask for an inclusion proof:
# 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}' | jqThe full client-side verification walk (hash chain → proof path → root → Ed25519 signature) is in the HTTP API under “Verification.”
5. Stream records back
Section titled “5. Stream records back”NDJSON, one Leaf per line. to_seq: 0 means “to the current end.”
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.
6. Go example
Section titled “6. Go example”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.
7. Error handling cheat sheet
Section titled “7. Error handling cheat sheet”| HTTP | code | Retry? | What to do |
|---|---|---|---|
| 200 | — | — | Persist the receipt.sequence if you need to prove later. |
| 400 | invalid_argument | No | Your request is malformed (missing payload, bad log_id chars, etc.). Fix the body. |
| 401 | unauthenticated | No | Refresh JWT or fix X-Tenant. Chassis rejects mismatches between JWT claim and header with this code too. |
| 404 | not_found | No | The log_id doesn’t match any routing rule — ops needs to add one. |
| 500 | internal | Yes, with backoff | Disk error, vault unreachable, signer failure. Workers behind the scenes back off and retry; you should too. |
8. Gotchas
Section titled “8. Gotchas”payloadis opaque, capped at 16 MiB. The service hashes it but never inspects. Compress or split your own large records before submitting.metadatacapped at 64 KiB total. Keep it small — high-cardinality fields belong in the payload.dedup_keyis reserved. audit.svc hasn’t shipped dedup semantics yet; do not rely on it to suppress duplicates./logsreturns 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_signatureverification. The service storesactor_signature/actor_key_idverbatim. Downstream verifiers do the actor-cert check; the service only signs the Merkle root. - Streaming
/readcannot signal mid-stream errors. A truncated response is a failure; resume from the last seensequence. - 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).
9. Where to go next
Section titled “9. Where to go next”- 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.