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:
- Agent bearer token — issued at enrollment, long-lived, tied to one agent record. Used on
/api/v1/ingest. Header: Authorization: Bearer <token>.
- Session JWT (HS256) — issued on login, 24-hour lifetime, tied to one user. Used on every other endpoint. Header:
Authorization: Bearer <jwt>.
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
| Method | Path | Auth | Description |
| GET | /api/v1/health | none | Liveness check. Returns {"status":"ok"}. Used by uptime monitors. |
Enrollment & auth
| Method | Path | Auth | Description |
| POST | /api/v1/enroll | username + password | Agent enrollment. Creates an agent record, returns a bearer token. |
| POST | /auth/login | username + password | Dashboard login. Returns a JWT valid for 24h. |
| GET | /api/v1/auth/me | JWT | Current user profile (username, role, trial end, plan). |
| POST | /api/v1/auth/update-email | JWT | Update the user's email address. |
| POST | /api/v1/auth/forgot-password | none | Request a password-reset email. Always returns 200 (anti-enumeration). |
| POST | /api/v1/auth/reset-password | reset token | Consume 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
| Method | Path | Auth | Description |
| POST | /api/v1/ingest | agent bearer | Submit 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},
{"pid": 1235, "name": "postgres", "user": "postgres", "cpu_percent": 1.1}
],
"listening_ports": [
{"port": 80, "protocol": "tcp", "process": "nginx"},
{"port": 5432, "protocol": "tcp", "process": "postgres"}
],
"logged_in_users": ["alice"]
}
Graph & topology
| Method | Path | Auth | Description |
| GET | /api/v1/graph | JWT | Returns {nodes, edges} in Cytoscape-ready format for the Graph view. |
| GET | /api/v1/topology | JWT | Compact list of hosts with counts (services, ports, identities) for the List view. |
Incidents
| Method | Path | Auth | Description |
| GET | /api/v1/incidents | JWT | List incidents for the current user. Query params: status, severity, limit, offset. |
| POST | /api/v1/incidents/{id}/acknowledge | JWT | Mark as acknowledged. |
| POST | /api/v1/incidents/{id}/dismiss | JWT | Mark as dismissed. |
| POST | /api/v1/incidents/{id}/remediate | JWT | Trigger on-demand remediation generation (Mode 2 — bypasses the auto-remediation that runs on incident creation). |
| POST | /api/v1/incidents/{id}/remediation/accept | JWT | Accept the proposed remediation (advisory — does not apply it; only updates status). |
| POST | /api/v1/incidents/{id}/remediation/reject | JWT | Reject the proposed remediation. |
| POST | /api/v1/incidents/{id}/remediation/applied | JWT | Mark remediation as applied. Kefal will verify on the next snapshot that the invariant is no longer violated. |
Invariants (collective defensive intelligence)
| Method | Path | Auth | Description |
| GET | /api/v1/invariants | JWT | List the invariants active for your tenant (32 builtins + any community-contributed ones that survived verification). |
| POST | /api/v1/invariants/contribute | JWT | Submit a new invariant. See schema below. |
| POST | /api/v1/invariants/{id}/verify | JWT | Trigger 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)
| Method | Path | Auth | Description |
| GET | /api/v1/billing/plans | JWT | List the three plans with prices and limits. |
| GET | /api/v1/billing/status | JWT | Current plan, trial/active state, next billing date. |
| POST | /api/v1/billing/checkout | JWT | Start a PayPal checkout for the requested plan. Returns an approval URL. |
| POST | /api/v1/billing/cancel | JWT | Cancel the active subscription (remains active until period end). |
| POST | /api/v1/billing/webhook | PayPal signature | PayPal webhook sink. Verified via PAYPAL_WEBHOOK_ID. |
Admin (admin role only)
| Method | Path | Auth | Description |
| GET | /api/v1/admin/dashboard | JWT (admin) | Platform-wide analytics: users, agents, snapshots, incidents per day. |
| GET | /api/v1/admin/users | JWT (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
200 OK — success, response body present.
400 Bad Request — payload validation failed. Body contains a Pydantic error list.
401 Unauthorized — missing, invalid, or expired token.
402 Payment Required — trial expired or subscription lapsed. Only on /api/v1/ingest.
403 Forbidden — authenticated but not authorized (e.g. non-admin calling admin endpoints).
404 Not Found — resource does not exist or is not visible to the caller.
429 Too Many Requests — rate limit hit (currently: /forgot-password 3/hour; more endpoints will be rate-limited in future releases).
500 Internal Server Error — unexpected server error. Correlate with your agent or browser timestamp and email us.
503 Service Unavailable — DB pool saturated or disconnected.