Skip to content
Talk to our solutions team

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
200Persist 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.