Skip to content

API Overview

Interactive documentation is available at /api/v1/docs (Swagger UI) and /api/v1/redoc once the server is running.


http://<host>:<port>/

Default: http://localhost:8000/


The server exposes the same functionality through two protocols on the same port:

ProtocolBase pathUse case
REST (HTTP/JSON)/Automation, CI, custom scripts
MCP (SSE / Streamable HTTP)/mcpAny MCP-compatible client or agent
flowchart TD
    A["1. GET /{analyzer}/rules\nFetch the security ruleset once\nReuse for every resource in the loop"] --> B
    B["2. POST /{analyzer}/{namespace}/ingest\nSend the raw plan\nReceive resource units[] with stable hashes"] --> C
    C{{"3. Loop until HTTP 208"}}
    C --> D["a. GET /{namespace}/resource/next\n→ one resource"]
    D --> E["b. Analyse using the ruleset\nfetched in step 1"]
    E --> F["c. PUT /resource/{hash}/analysis\n→ store findings"]
    F --> C
    C -->|HTTP 208 — all done| G["4. GET /{namespace}/report\nRetrieve consolidated\nMarkdown or HTML report"]

Server-side alternative: POST /{analyzer}/{namespace}/analyze runs the whole workflow above on the server itself — the caller supplies an LLM provider/model (and an API key, or relies on one stored for the org) and gets the report back directly (sync for small inputs) or a job to poll (async for large inputs). See POST /{analyzer}/{namespace}/analyze.


TagPrefixPurpose
health/healthLiveness probe (no auth)
session/auth/register, /auth/session…, /auth/me…Human user registration/login/logout, session introspection for dashboard bootstrap (no API-key auth)
auth/auth/keysService account (named, IP-filterable API key) management (org-admin only)
sso/orgs/{org_id}/sso, /auth/sso/…Per-org OIDC connection CRUD (org-admin) + the public discover/start/callback login flow — see Single sign-on
orgs/orgs/…Org info, members, invitation links, analyzer load/unload, LLM credentials, billing quota/usage, report-link TTL
teams/orgs/{org_id}/teams/…Teams CRUD + member add/remove — pure organizational grouping, no access-control effect (org-scoped; create/rename/delete/add/remove-member are org-admin only)
resources/resource/…, /{namespace}/…CRUD + namespace ops (org-scoped)
reports/reports/{token}Public, time-limited HTML report access (no auth — see Report links)
invitations/invite/{token}, /invite/{token}/acceptPreview is public; accept requires a session — see Invitations
analyzers/analyzers, /{analyzer}/…Discovery, rules, ingest, server-side analyze (org-scoped)
rules/orgs/{org_id}/rules/…Custom rules CRUD — create, list/get, partial update (including the enabled/requires_review toggles), delete (org-scoped; create/update/delete are org-admin only)
report-templates/orgs/{org_id}/report-templates/…Custom report templates CRUD — create, list/get, partial update (including the is_default toggle), delete, seed-builtin, preview (org-scoped; create/update/delete are org-admin only)
jobs/jobs/…Async /analyze job status polling (org-scoped)
findings/findings/…Finding triage — list, summary, PATCH status incl. needs_review (org-scoped)
integrations/orgs/{org_id}/integrations/github[/manifest|/app], /orgs/{org_id}/integrations/review-webhook, /integrations/github/manifest/callback, /integrations/github/webhookGitHub App connect/disconnect (org-admin), the manifest flow for a custom per-org App, the public webhook receiver, + the needs-review outbound webhook (org-admin)
metrics/metrics/…Telemetry — spans, summary, key usage (org-admin only, org-scoped)
admin/admin/…Platform-wide, cross-tenant operator views — requires is_superadmin, not any org role. See Platform Admin.

Every request resolves to a principal bound to exactly one organization (org_id) — there is no cross-org access without switching credentials. Two kinds of principal exist, both authenticate to the same role model:

  • API key (X-API-Key header or ?api_key=) — machine principal for CI/MCP clients, created via POST /auth/keys.
  • Session cookie (mcp_session, set by POST /auth/session) — human principal for interactive/dashboard use, obtained via POST /auth/register, POST /auth/session, or a completed SSO login (GET /auth/sso/callback) — all three set the same cookie, so nothing downstream distinguishes how a session was obtained.

Authentication is optional and controlled by auth.api_key in the config file — when null (the default), the server runs as an anonymous owner of a default org with no authentication, suitable for local dev or private networks only.

A single ordering applies to both API keys and human memberships: viewer < member < admin < owner.

RoleCan access
owner / adminEverything member can, plus key management, org member management, team management, analyzer load/unload, creating/editing/deleting custom rules, configuring the needs-review webhook, and metrics
memberAnalyzer discovery/rules/ingest, resource CRUD, reports, reading (but not writing) custom rules
viewerReserved for future read-only use; not yet distinguished from member in Phase 0+1

is_superadmin is a separate axis, not a fifth role above owner. It answers “can this principal see across every org,” which owning even every org you belong to doesn’t imply — a user can be owner of three orgs and a superadmin of none. Gates only /admin/…. See Platform Admin.

org_name and invite_token are both optional and mutually exclusive — send one, the other, or neither:

Terminal window
# Create a new org, owned by you (the original, still-default behavior)
curl -X POST http://localhost:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "at-least-8-chars", "org_name": "My Org"}'
# → sets the mcp_session cookie; you now own a brand-new organization
Terminal window
# Join an org via an invitation link instead — see Invitations below
curl -X POST http://localhost:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "at-least-8-chars", "invite_token": "xK9mZpQ..."}'
Terminal window
# Neither — an org-less account. Every org-scoped endpoint 403s until you
# create an org (POST /orgs) or accept an invite.
curl -X POST http://localhost:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "at-least-8-chars"}'
Terminal window
curl -X POST http://localhost:8000/auth/session \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "at-least-8-chars"}' \
-c cookies.txt
# → 200 even for an org-less account (org_id/role null in the response) —
# this used to 401 with "no organization membership"; org-less is now a
# real, intended account state, not an error.

An already-registered, org-less account “graduates” by calling POST /orgs (create a new org, same endpoint an existing user calls to add a second one) or by opening an invitation link — see Invitations.

auth.api_key in the config is the bootstrap admin key. It is permanent, never stored in the database, cannot be revoked, and always resolves to the default org as owner — treat it like a root password and keep it secret.

Use an admin/owner credential (bootstrap key or a logged-in session) to create runtime keys — “service accounts” in dashboard terms — scoped to that credential’s org, optionally restricted to a set of source IPs:

Terminal window
curl -X POST http://localhost:8000/auth/keys \
-H "X-API-Key: <admin-key>" \
-H "Content-Type: application/json" \
-d '{"role": "member", "name": "ci-pipeline", "ip_allowlist": ["203.0.113.0/24"]}'
{
"key_id": "3f4a...",
"key": "xK9mZ...",
"role": "member",
"name": "ci-pipeline",
"ip_allowlist": ["203.0.113.0/24"],
"created_at": "2026-01-01T00:00:00Z"
}

The raw key is returned once — store it securely. name and ip_allowlist are both optional; a request using this key from outside ip_allowlist gets 403 (the key is valid, just not usable from there) rather than the 401 an unrecognised key gets.

Terminal window
curl -X DELETE http://localhost:8000/auth/keys/<key_id> \
-H "X-API-Key: <admin-key>"

Only a key belonging to the caller’s own org can be revoked — attempting to revoke another org’s key returns 404, not 403 (it gives no signal that the key exists at all).

See Auth endpoints for the full API reference.

Each org can connect its own OIDC identity provider instead of relying on passwords — POST/GET/PATCH/DELETE /orgs/{org_id}/sso (org-admin) manages the connection; POST /auth/sso/discover, GET /auth/sso/{org_id}/start, and GET /auth/sso/callback (all public — the caller isn’t authenticated yet) implement the login itself. A completed login resolves the asserted email against existing users (linking a local-password account rather than duplicating it) and JIT-provisions a membership at the connection’s default_role if one doesn’t already exist, without ever downgrading an existing one. See Quick Start: SSO & SCIM — Single sign-on for the setup walkthrough and API Endpoints — SSO for the full request/response reference.


Every response includes an X-Request-ID header. Supply your own value in the request to correlate log entries:

X-Request-ID: my-run-42

All errors return JSON:

{"detail": "human-readable message"}
StatusMeaning
400Bad request (e.g. unknown filter field)
401Missing or invalid credentials (API key or session), or an invalid GitHub webhook signature
402Org’s monthly LLM spend cap already met/exceeded (/analyze pre-flight check — see Billing / quota)
403Authenticated but insufficient role (admin required); or an SSO login whose asserted email’s domain isn’t in the connection’s allowed_domains
404Resource / analyzer not found, or a cross-org access attempt
208All resources in namespace already analysed — exit the loop
409Key already exists (safe to ignore on ingest)
413Input too large (exceeds server.max_input_size_mb)
422Schema validation error or invalid analysis payload
500Unhandled server error
501Analyzer not yet implemented; a webhook whose installation has neither its own App nor the platform’s config.github configured; or the manifest-flow trigger without server.public_base_url set
502LLM provider returned output that couldn’t be parsed (/analyze sync path only); an SSO identity provider was unreachable during login (GET /auth/sso/{org_id}/start); or POST /admin/storage/prepare-mongodb/POST /admin/storage/switch couldn’t reach the target MongoDB