HTTP API (/v1)
The ScaiLog server is a FastAPI app that exposes one versioned surface under
/v1, plus a Prometheus /metrics endpoint. Every /v1 route except the
health/status/report-key reads requires a bearer token; authorization is by
capability, derived from the caller's role. This page enumerates every route
in server/app.py: method, path, purpose, the capability it requires, and the
fields that matter in the request and response.
Authentication#
Every protected endpoint expects an Authorization: Bearer <token> header. The
token is either a built-in API key (prefix slk_, the base mechanism) or, when
the optional OIDC plugin is configured, a ScaiKey-issued id_token. API keys are
verified locally; OIDC tokens are validated against the configured issuer and
mapped to a role via the scailog_role claim.
1 2 | |
A missing or malformed header returns 401. A valid token whose role lacks the
required capability returns 403 (role '<role>' lacks capability '<cap>'). A
token scoped to another tenant or service returns 403, except where the
handler deliberately returns 404 to keep a resource indistinguishable from
absent (single-entry reads, key revocation).
Roles and capabilities#
Capabilities are the unit of authorization; roles are fixed bundles of them.
| Role | Capabilities |
|---|---|
log_writer |
ingest |
log_reader |
query |
log_reader_plus |
query, decrypt_one |
log_admin |
query, decrypt_one, admin |
dpo |
ingest, query, decrypt_one, admin, dpo |
The bootstrap key minted on first start is a dpo key scoped to all tenants
(*). dpo is a full superset and includes ingest so a standalone operator
can push directly. Full role/key mechanics are in
Configuration and the auth model.
Endpoints at a glance#
| Method | Path | Capability | Purpose |
|---|---|---|---|
| POST | /v1/ingest |
ingest |
Accept a batch or JSON list of log entries |
| POST | /v1/agent/enroll |
ingest |
Return the unwrapped tenant KEK to an agent |
| POST | /v1/agent/dek |
ingest |
Mint/return a wrapped per-subject DEK |
| GET | /v1/whoami |
any authenticated | Resolve the caller's role, scope, capabilities |
| GET | /v1/tenants |
query |
List tenant ids visible to the key |
| GET | /v1/query |
query |
Query stored entries (PI as placeholders) |
| GET | /v1/tail |
query |
Server-sent-events live tail |
| GET | /v1/entries/{event_id} |
query |
Fetch one entry; decrypt_one reveals PI |
| GET | /v1/entries/{event_id}/explain |
query |
Explain the policy decision for an entry |
| GET | /v1/sites |
query |
List observed call sites |
| POST | /v1/sites/manifest |
admin |
Upload a CI call-site manifest |
| POST | /v1/dsar |
dpo |
Run a DSAR and return a signed report |
| POST | /v1/erase |
dpo |
Crypto-shred a subject; return a signed receipt |
| GET | /v1/receipts/{receipt_id} |
dpo |
Fetch a stored erasure receipt |
| GET | /v1/policies |
admin |
Get the active policy + version history |
| PUT | /v1/policies |
admin |
Validate and store a new policy version |
| GET | /v1/keys |
admin |
List API keys (tenant-scoped) |
| POST | /v1/keys |
admin |
Create an API key (returns plaintext once) |
| DELETE | /v1/keys/{key_id} |
admin |
Revoke an API key |
| GET | /v1/audit/findings |
admin |
List audit findings by status |
| POST | /v1/audit/findings/{finding_id}/resolve |
admin |
Resolve a finding |
| POST | /v1/audit/redact |
dpo |
Scrub audit-found untagged PI at a call site |
| GET | /v1/health |
none | Liveness probe |
| GET | /v1/status |
none | Counters + active report public key |
| GET | /v1/report-key |
none | The active report public key |
| GET | /v1/report-keys |
none | All report public keys (active + retired) |
| POST | /v1/report-keys/rotate |
dpo |
Rotate the report signing key |
| GET | /metrics |
none | Prometheus text-format counters |
Ingest and agent#
These are the write path. Agents authenticate with an ingest-capable API key
and never touch OIDC, keeping ingestion bootable when identity services are
down.
POST /v1/ingest#
Accepts log entries. If the request carries an x-slwp-batch-id header the body
is decoded as an SLWP batch (zstd/NDJSON) and its checksum is verified;
otherwise the body is JSON {"entries": [...]}. Batch ingestion is idempotent:
a batch id already seen returns {"status": "duplicate"}, and a checksum
mismatch returns 400 BATCH_CHECKSUM_MISMATCH.
1 2 3 4 5 6 7 | |
Response reports counts and per-entry rejections (each with a code such as
SUBJECT_REQUIRED, SITE_INVALID, RESERVED_FIELD, and a detail):
1 | |
POST /v1/agent/enroll#
Returns the unwrapped tenant KEK (base64) to an authenticated agent. The KEK
lives only in agent memory. Body: {"tenant": "<id>"}. Response:
{"tenant": "<id>", "kek": "<base64>"}. The tenant is created on first use.
POST /v1/agent/dek#
Mints on first sight and returns the wrapped per-subject DEK; the agent
unwraps it with its in-memory KEK. Body: {"tenant": "<id>", "subject": "<id>"}.
Response: {"key_ref": "...", "wrapped_dek": "<base64>"}. An already-erased
subject returns 410.
Query and read#
The read path never bulk-decrypts PI. Query results carry encrypted PI as
placeholders; the only decryption points are a single-entry read by a
decrypt_one principal (audited) and a DSAR run.
GET /v1/whoami#
Returns the calling principal so a client can validate a key and drive RBAC-aware UI. Requires only a valid token (no specific capability).
1 2 | |
GET /v1/tenants#
Distinct tenant ids visible to the key (for a tenant switcher). A tenant-scoped
key sees only its own tenant. Response: {"tenants": ["default", ...]}.
GET /v1/query#
Query stored entries. Query parameters: tenant (required), service, site
(the call_site_id), level, since, until, subject, q (FTS over
message + plaintext fields), limit (default 100). A subject-filtered
query requires dpo. A service filter is rejected with 403 if the key is
not scoped to that service.
1 2 | |
Response: {"entries": [<entry>...], "count": <n>}. Each entry includes
event_id, tenant, service, environment, host, call_site_id,
level, ts, ingested_at, message, fields, data_subject_id,
trace_id, request_id, and erased. If the entry has PI, a pi object maps
each field name to {"type", "action", "value"} where an encrypted value is
"[ENCRYPTED]".
GET /v1/tail#
A live tail as text/event-stream (SSE). Query parameters: tenant
(required), service, level, q. Each event is a data: line carrying the
same entry shape as /v1/query. The stream polls roughly once a second and
de-dupes across poll boundaries.
GET /v1/entries/{event_id}#
Fetch one entry. Requires tenant as a query parameter. Returns 404 if the
entry is absent or the key is not scoped to its service (indistinguishable). If
the caller has decrypt_one and the entry has PI, a non-erased subject, and a
subject id, the response adds a pi_values object with the decrypted field
values — this single-entry decrypt is counted for audit.
GET /v1/entries/{event_id}/explain#
Explains why an entry was stored as it was: the resolved policy version and,
per PI field, its pi_type, stored_action, resolved_action, rule_index,
and reason. Requires tenant. Response includes event_id, tenant,
service, environment, call_site_id, policy_version, policy_found, and
fields.
GET /v1/sites#
Lists observed call sites. Query parameters: tenant (required), service,
auto_only (bool), pi_only (bool). Response {"sites": [...]}; each site
carries service, call_site_id, hit_count, is_auto, declares_pi, and
(when a manifest was pushed) declared_pi, manifest_version, owner.
POST /v1/sites/manifest#
Uploads a CI call-site manifest so scailog sites --diff can flag drift.
Requires admin. Body: {"service": "...", "version": "...", "sites": [...], "tenant": "...", "owner": "..."} (tenant defaults to the key's tenant scope
or default). Response: {"upserted": <n>, "service": "..."}.
DSAR, erasure, and receipts#
These are the GDPR endpoints and all require the dpo capability. DSAR and
erasure return Ed25519-signed documents that embed the signing key's public key
and key_id, so they stay verifiable across key rotations.
POST /v1/dsar#
Runs a Data Subject Access Request and returns a signed report. Body:
{"tenant": "...", "subject": "..."}. The report includes report_id, type
("dsar"), tenant, subject, requested_by, generated_at, entry_count,
time_range, call_sites, subject_erased, entries, and a signature.
POST /v1/erase#
Crypto-shreds a subject (destroys the per-subject DEK) and tombstones or
deletes affected entries. Body requires confirm: true (else 400); optional
mode is "tombstone" (default) or "delete". Idempotent: re-erasing an
already-erased subject returns the original receipt. The signed receipt
includes receipt_id, type ("erasure"), tenant, subject,
requested_by, requested_at, key_destroyed_at, entries_tombstoned,
entries_deleted, erasure_mode, absolute_after, and a signature.
1 2 3 4 5 | |
GET /v1/receipts/{receipt_id}#
Returns a stored erasure receipt document. 404 if unknown.
Policies#
Policy management requires admin. Policies are declarative YAML, validated on
write and versioned forever.
GET /v1/policies#
Returns the active policy for a tenant. Query parameter tenant (required).
Response: {"version": <n>, "yaml": "...", "history": [...]}.
PUT /v1/policies#
Validates and stores a new policy version. Body: {"tenant": "...", "yaml": "..."}. Malformed YAML or an invalid action/pi_type returns 400 invalid policy: .... Response: {"version": <n>, "warnings": [...]}.
API keys#
Key management requires admin. A tenant-scoped admin is contained to its own
tenant and cannot widen service scope; only a dpo may mint a key carrying the
dpo capability.
GET /v1/keys#
Lists keys. A tenant-scoped admin sees only its tenant's keys. Response
{"keys": [...]} with key_id, prefix, role, tenant_scope,
service_scope, created_at, expires_at, revoked_at.
POST /v1/keys#
Creates a key and returns the plaintext once. Body: {"role": "...", "tenant_scope": "...", "service_scope": [...], "expires_at": "...", "rate_limit_per_min": <n>, "ip_allowlist": [...]} (only role is required;
scopes default to the creator's). Response: {"key_id": "...", "key": "slk_...", "note": "shown once — store it now"}.
DELETE /v1/keys/{key_id}#
Revokes a key. 404 if unknown, already revoked, or outside the caller's
tenant scope. Response: {"revoked": "<key_id>"}.
Audit#
Audit-finding management requires admin; the redaction endpoint rewrites
stored personal data and therefore requires dpo.
GET /v1/audit/findings#
Lists findings. Query parameters: tenant (required), status (default
"open"). Response: {"findings": [...]}.
POST /v1/audit/findings/{finding_id}/resolve#
Resolves a finding. Body: {"tenant": "...", "action": "...", "status": "..."} (status defaults to resolved; tenant falls back to the key's
scope). 404 if not found. Response: {"resolved": "<finding_id>"}.
POST /v1/audit/redact#
Targeted rewrite of audit-found untagged PI: scrubs named plaintext fields to
[AUDIT_REDACTED] across one call site. Requires dpo. Body: {"tenant": "...", "service": "...", "call_site_id": "...", "fields": ["..."], "finding_id": "..."}. service and fields are required (400 otherwise). Response:
{"call_site_id", "service", "fields", "affected", "resolved_finding"}.
Health, status, and report keys#
These reads are unauthenticated. The report-key endpoints publish the public keys used to sign DSAR reports and erasure receipts, so anyone can verify a downloaded document offline.
GET /v1/health#
Liveness. Response: {"status": "ok", "ts": "<rfc3339>"}.
GET /v1/status#
Response: {"status": "ok", "counters": {...}, "report_public_key": "...", "ts": "<rfc3339>"}.
GET /v1/report-key#
The active report public key (back-compat). Response: {"alg": "Ed25519", "public_key": "...", "key_id": "..."}. Prefer /v1/report-keys for verifying
documents that may have been signed by a now-retired key.
GET /v1/report-keys#
All published report public keys, active and retired, for offline verification
across rotations. Response: {"alg": "Ed25519", "active": "<key_id>", "keys": [{"key_id", "status", "created_at", "retired_at", "public_key"}...]}.
POST /v1/report-keys/rotate#
Rotates the report signing key: mints a new active key and retires (but keeps
publishing) the old one. Requires dpo. Response: {"active": "<new_id>", "retired": "<old_id>", "keys": [...]}.
GET /metrics#
Prometheus text exposition (text/plain; version=0.0.4) of the server
counters. Unauthenticated.