Plattform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Modelle Tools & Services
Lösungen
Organisationen Entwickler Internet Service Provider Managed Service Provider AI-in-a-Box
Ressourcen
Support Documentation Blog Downloads
Unternehmen
Über uns Forschung Karriere Investieren Kontakt
Anmelden

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.

bash
1
2
curl -sS http://127.0.0.1:8080/v1/query?tenant=default \
  -H "Authorization: Bearer slk_read_XXXXXXXXXXXX"

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.

http
1
2
3
4
5
6
7
POST /v1/ingest
Authorization: Bearer slk_agent_XXXXXXXXXXXX
Content-Type: application/json

{"entries": [{"tenant": "default", "service": "billing",
              "site": "BILLING-CHARGE-OK", "level": "info",
              "msg": "charge captured", "ts": "2026-07-13T10:00:00Z"}]}

Response reports counts and per-entry rejections (each with a code such as SUBJECT_REQUIRED, SITE_INVALID, RESERVED_FIELD, and a detail):

json
1
{"accepted": 1, "rejected": [{"event_id": "...", "code": "SITE_INVALID", "detail": "..."}]}

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

json
1
2
{"key_id": "...", "role": "log_admin", "tenant_scope": "default",
 "service_scope": ["billing"], "capabilities": ["admin", "decrypt_one", "query"]}

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.

bash
1
2
curl -sS "http://127.0.0.1:8080/v1/query?tenant=default&level=error&limit=50" \
  -H "Authorization: Bearer slk_read_XXXXXXXXXXXX"

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.

http
1
2
3
4
5
POST /v1/erase
Authorization: Bearer slk_dpo_XXXXXXXXXXXX
Content-Type: application/json

{"tenant": "default", "subject": "user-42", "confirm": true, "mode": "tombstone"}

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.

Updated 2026-07-13 09:41:43 View source (.md) rev 2