API reference

All endpoints are served at https://kefal.dev. Payloads are JSON. Content-Type is application/json unless noted.

Authentication

Kefal uses two distinct auth models, depending on who's calling:

Tenant isolation Every authenticated endpoint scopes its DB reads and writes by the authenticated user_id. One user cannot see another user's agents, graph, incidents, or billing state. This is enforced at the query level, not at the application layer.

Health

MethodPathAuthDescription
GET/api/v1/healthnoneLiveness check. Returns {"status":"ok"}. Used by uptime monitors.

Enrollment & auth

MethodPathAuthDescription
POST/api/v1/enrollusername + passwordAgent enrollment. Creates an agent record, returns a bearer token.
POST/auth/loginusername + passwordDashboard login. Returns a JWT valid for 24h.
GET/api/v1/auth/meJWTCurrent user profile (username, role, trial end, plan).
POST/api/v1/auth/update-emailJWTUpdate the user's email address.
POST/api/v1/auth/forgot-passwordnoneRequest a password-reset email. Always returns 200 (anti-enumeration).
POST/api/v1/auth/reset-passwordreset tokenConsume a reset token and set a new password.

Enroll example

curl -X POST https://kefal.dev/api/v1/enroll \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"..."}'

# → 200
# {"token":"eyJhbGciOiJIU…","agent_id":"3d30dce8-e6b8-4624-8a51-8d1c430c2f67"}

Ingest

MethodPathAuthDescription
POST/api/v1/ingestagent bearerSubmit one snapshot. Returns 200 on success, 402 if the trial/subscription has expired.

Snapshot schema

{
  "agent_id": "3d30dce8-e6b8-4624-8a51-8d1c430c2f67",
  "timestamp": "2026-04-21T10:00:00Z",
  "hostname": "web-01",
  "os": "linux",
  "kernel": "6.8.0-41-generic",
  "uptime_seconds": 1234567,
  "ip_addresses": ["192.168.1.5", "10.0.0.12"],
  "processes": [
    {"pid": 1234, "name": "nginx",    "user": "www-data", "cpu_percent": 0.2, "cmdline": "nginx: master process", "exe": "/usr/sbin/nginx"},
    {"pid": 1235, "name": "postgres", "user": "postgres", "cpu_percent": 1.1, "cmdline": "/usr/lib/postgresql/16/bin/postgres", "exe": "/usr/lib/postgresql/16/bin/postgres"}
  ],
  "listening_ports": [
    {"port": 80,   "protocol": "tcp", "process": "nginx",    "address": "0.0.0.0"},
    {"port": 5432, "protocol": "tcp", "process": "postgres", "address": "127.0.0.1"}
  ],
  "logged_in_users": ["alice"]
}

Agent v0.11+ enriches this payload: listening_ports[].address is the bind IP (so the server can tell an internet-exposed service from a loopback-only one), and processes[].cmdline/exe enable command-line and path-based detections. The agent also always includes processes that own a listening port — even at ~0 % CPU — so a low-CPU backdoor is never dropped. Older fields stay backward-compatible.

Graph & topology

MethodPathAuthDescription
GET/api/v1/graphJWTReturns {nodes, edges} in Cytoscape-ready format for the Graph view.
GET/api/v1/topologyJWTCompact list of hosts with counts (services, ports, identities) for the List view.

Incidents

MethodPathAuthDescription
GET/api/v1/incidentsJWTList incidents for the current user. Query params: status, severity, limit, offset. Each incident now carries a risk_score (0–100 dynamic risk) and an explanation object (what / why / action in plain language).
GET/api/v1/postureJWTOne-number security posture over the caller’s open incidents: score (0–100), grade (A–F), open_count, worst_risk, and a one-line summary. Driven by the worst open incident plus a bounded penalty for accumulation.
POST/api/v1/incidents/{id}/acknowledgeJWTMark as acknowledged.
POST/api/v1/incidents/{id}/dismissJWTMark as dismissed.
POST/api/v1/incidents/{id}/remediateJWTTrigger on-demand remediation generation (Mode 2 — bypasses the auto-remediation that runs on incident creation).
POST/api/v1/incidents/{id}/remediation/acceptJWTAccept the proposed remediation (advisory — does not apply it; only updates status).
POST/api/v1/incidents/{id}/remediation/rejectJWTReject the proposed remediation.
POST/api/v1/incidents/{id}/remediation/appliedJWTMark remediation as applied. Kefal will verify on the next snapshot that the invariant is no longer violated.

Invariants (collective defensive intelligence)

MethodPathAuthDescription
GET/api/v1/invariantsJWTList the invariants active for your tenant (32 builtins + any community-contributed ones that survived verification).
POST/api/v1/invariants/contributeJWTSubmit a new invariant. See schema below.
POST/api/v1/invariants/{id}/verifyJWTTrigger adversarial verification on a pending invariant (constructive + red-team LLM prompts).

Contribution schema

{
  "name":           "A short unique name",
  "invariant_type": "structural | temporal | behavioral",
  "predicate": {
    "kind":   "process_in_suspicious_path",
    "params": { "user": "root", "suspicious_paths": ["/tmp"] }
  },
  "severity": "low | medium | high | critical",
  "metadata": {
    "description": "What this invariant detects, in one sentence.",
    "pitch":       "Why SMBs should care."
  }
}

Billing (PayPal)

MethodPathAuthDescription
GET/api/v1/billing/plansJWTList the three plans with prices and limits.
GET/api/v1/billing/statusJWTCurrent plan, trial/active state, next billing date.
POST/api/v1/billing/checkoutJWTStart a PayPal checkout for the requested plan. Returns an approval URL.
POST/api/v1/billing/cancelJWTCancel the active subscription (remains active until period end).
POST/api/v1/billing/webhookPayPal signaturePayPal webhook sink. Verified via PAYPAL_WEBHOOK_ID.

Admin (admin role only)

MethodPathAuthDescription
GET/api/v1/admin/dashboardJWT (admin)Platform-wide analytics: users, agents, snapshots, incidents per day.
GET/api/v1/admin/usersJWT (admin)List all users with plan + activity state.
GET/api/v1/admin/users/{user_id}JWT (admin)Detail view of one user — agents, recent incidents, billing.

Status codes