Skip to content

kaveo · Reference

CLI and API reference

Reference for the kaveo CLI (commands, flags, exit codes, CI gating), authentication, the /v1 REST API, streaming, error envelope and health endpoints.

On this page

The kaveo CLI#

kaveo is a command-line client built for CI first. It is already installed in the api and worker images. The CLI talks HTTP to /v1/* only and never opens the database. RBAC and row-level security are enforced on the server, so the CLI never sees more than the console would show the same user.

Point it at kaveo#

Each setting is resolved in this order: flag, then environment variable, then config file.

SettingFlagEnvironmentDefault
API origin--urlKAVEO_API_URLhttp://localhost:8000
Credential--tokenKAVEO_API_TOKENnone
Config directorynoneKAVEO_CONFIG_HOME~/.config/kaveo, file config.json

The default URL is the api container's own address, so it works when you run the CLI inside that container. Compose publishes only Caddy's ports, and Caddy forwards /v1/* to the api. From anywhere else, set the URL to your kaveo origin, for example https://kaveo.example.com, or http://localhost for a local stack.

kaveo auth login stores the origin and a session token in the config file and writes it with mode 0600.

Global flags#

Every command accepts these flags. Put them after the full command, for example kaveo findings list --output table. A flag placed before the subcommand, as in kaveo --url ... scan list, is ignored.

  • --url and --token override the environment and the config file.
  • --output picks json (the default), table or csv. Results go to stdout. Progress, notes and warnings go to stderr, so kaveo findings list | jq stays clean.
  • --yes turns off prompts. With auth login, a missing password then fails as a usage error instead of prompting.

Commands#

CommandWhat it does
kaveo auth login --email E [--password P] [--mfa-code C]Exchanges a password for a session. Without --password it reads KAVEO_PASSWORD, then prompts.
kaveo auth logoutRevokes the session on the server if it can, then deletes the local credential.
kaveo auth whoamiShows the authenticated principal.
kaveo auth token create --name N [--scope S]... [--ttl-days D]Mints an API token. Scopes default to view_findings and run_scan. Without --ttl-days the token does not expire. The token is printed once.
kaveo auth token listLists your API tokens.
kaveo auth token revoke IDRevokes an API token.
kaveo accounts listLists connected cloud accounts.
kaveo scan start --account ID [--region R]... [--wait] [--fail-on SEVERITY] [--timeout 900]Queues a scan. Leave out --region to scan every enabled region.
kaveo scan status SCAN_IDShows one scan.
kaveo scan list --account ID [--limit 20]Lists an account's scans. The API rejects a --limit above 50.
kaveo findings list [--scan ID] [--severity S] [--source S] [--include-suppressed] [--limit 50] [--offset 0]Lists findings. The API rejects a --limit above 500. When more exist, a note on stderr says how many you got.
kaveo findings get IDShows one finding with its evidence ids.
kaveo findings export [--format F] [--scan ID] [--severity S] [--include-suppressed] [--out FILE]Exports for a SIEM. F is ocsf (the default), asff, csv or json. It warns on stderr when the export looks like it hit the 10,000-row cap.

--account takes kaveo's account id, the id column of kaveo accounts list, not the 12-digit AWS account number.

A session from auth login expires after KAVEO_SESSION_TTL_HOURS (168 hours by default) and cannot be refreshed. For CI, mint an API token.

Gate a pipeline#

yaml
- name: kaveo scan gate
  env:
    KAVEO_API_URL: https://kaveo.example.com
    KAVEO_API_TOKEN: ${{ secrets.KAVEO_API_TOKEN }}
    KAVEO_ACCOUNT_ID: ${{ vars.KAVEO_ACCOUNT_ID }}
  run: kaveo scan start --account "$KAVEO_ACCOUNT_ID" --wait --fail-on high

The runner needs the kaveo package installed. If your runner can pull the kaveo-api image you built, you can run the CLI from that image instead:

bash
docker run --rm -e KAVEO_API_URL -e KAVEO_API_TOKEN kaveo-api:latest \
  kaveo scan start --account "$KAVEO_ACCOUNT_ID" --wait --fail-on high

How the gate behaves:

  • --fail-on requires --wait. Without it the command exits 2, so a gate never passes without a result.
  • The CLI polls GET /v1/scans/{scan_id} every 2 seconds until the status is succeeded, failed or partial. --timeout bounds the wait, in seconds.
  • Progress goes to stderr, and only on a TTY, so CI logs stay quiet.
  • Severities run critical, high, medium, low, info. --fail-on high also fires on critical.
  • The gate counts non-suppressed findings of every source, including native AWS findings.
  • A scan that ends failed exits 4. So does a scan that is still running when the timeout hits. Check it later with kaveo scan status.

Exit codes#

CodeNameMeaning
0OKThe command worked. With --fail-on, nothing was at or above the threshold.
1THRESHOLDThe finished scan has findings at or above --fail-on.
2USAGEA bad flag or missing argument, --fail-on without --wait, or an API answer of 400, 404, 409 or 422.
3AUTHNo credential was found, or the API answered 401 or 403.
4APIThe API was unreachable or timed out, or answered with any other error status, such as 429 or a 5xx. Also a scan that ended failed, or a wait that timed out.
5ENTITLEMENTThe API answered 402: the org's plan refused the operation.

Codes 1 and 4 are kept apart deliberately. "The scan found something" and "kaveo did not answer" never share a code, so an outage never reads as a pass. Branch on the exact code in your pipeline, not on any non-zero value.

API authentication#

  • Sessions. POST /v1/auth/login takes email, password and, when the user has TOTP MFA, mfa_code. It returns token and expires_at, and also sets the httpOnly kaveo_session cookie that the console uses. Send the token as Authorization: Bearer <token>. After repeated failed logins, the email is locked out for a cool-down period.
  • API tokens. Tokens start with kv_api_ and are meant for scripts and CI. Create one with POST /v1/api-tokens and a body of name, optional scopes and optional ttl_days (1 to 3650). The plaintext appears once in the response, and kaveo stores only a SHA-256 hash.
  • Scopes. A token can do only what both its owner's role and its scopes allow. Scopes are the permission names: view_findings, run_scan, investigate, share_report, triage_findings, approve_remediation, manage_accounts and manage_users. The default, view_findings plus run_scan, is enough for a CI gate and cannot change your cloud.
  • MCP tokens. Tokens that start with kv_mcp_ work only on POST /mcp. See MCP server and integrations.
  • Org selection. Send X-Kaveo-Org: <org-id> to act in another org you belong to. Without the header, requests use your home org. Platform superadmins can name any org.
  • Pending accounts. While KAVEO_APPROVAL_REQUIRED is on (the default), a user a superadmin has not approved yet gets a 403 on every authenticated route except GET /v1/auth/me and POST /v1/auth/logout.
  • Request ids. Responses carry X-Request-ID. If you send your own value in that header, kaveo uses it, so you can match a client error to a server log line. The one exception is a rate-limit 429, which is answered before a request id is assigned.

Endpoints#

The Needs column shows the permission a route checks. A session needs a role that grants it, and an API token needs it as a scope too. Viewers hold view_findings. Analysts add run_scan, investigate, share_report and triage_findings. Admins hold every permission. Your role in an org comes from your membership there: owners and admins act as admins, analysts as analysts, and viewers and plain members as viewers.

Auth and your account#

RouteNeedsNotes
POST /v1/auth/loginnonePassword login. See above.
POST /v1/auth/logoutsigned inRevokes the current session.
GET /v1/auth/mesigned inThe signed-in principal. Works for a pending user.
POST /v1/auth/signupnoneSelf-serve signup. Returns 403 while signup is off, which is the default.
POST /v1/auth/password/forgotnoneEmails a reset token. Always returns 204, whether or not the address has an account.
POST /v1/auth/password/resetnoneRedeems a single-use reset token, sets the new password and revokes the user's live sessions.
POST /v1/auth/email/verifynoneRedeems a single-use email verification token.
GET /v1/auth/providersnoneThe SSO providers this server is configured for.
GET /v1/auth/{provider}/loginnoneStarts SSO. provider is google, microsoft, github or oidc.
GET /v1/auth/{provider}/callbacknoneCompletes SSO and starts a session.
GET /v1/auth/login, GET /v1/auth/callbacknoneOIDC redirect and callback.
GET /v1/auth/saml/login, POST /v1/auth/saml/acsnoneSAML SP-initiated login and assertion consumer.
GET /v1/auth/mfa/statussigned inWhether TOTP MFA is enrolled and confirmed.
POST /v1/auth/mfa/enrollsigned inReturns the one-time secret, a provisioning URI and recovery codes. Returns 409 if MFA is already confirmed.
POST /v1/auth/mfa/verifysigned inConfirms a pending enrollment, or checks a code once MFA is active.
POST /v1/auth/mfa/disablesigned inNeeds a valid current code. A session alone is not enough.
GET /v1/me/preferences, PUT /v1/me/preferencessigned inConsole preferences. PUT replaces the whole object.
GET /v1/api-tokens, POST /v1/api-tokensview_findingsList or mint your API tokens.
DELETE /v1/api-tokens/{token_id}view_findingsRevokes a token.

Accounts and onboarding#

RouteNeedsNotes
GET /v1/accountsview_findingsRegistered cloud accounts.
POST /v1/accountsmanage_accountsRegisters an account, or updates it if it is already registered. A new account over the plan's cap gets a 402. See Connect cloud accounts.
DELETE /v1/accounts/{account_id}manage_accountsDeletes the account and its scans, findings and resources.
POST /v1/accounts/{account_id}/verifymanage_accountsAssumes the read-only role to confirm access. On the first success for an account with no scans, it queues the first scan.
PUT /v1/accounts/{account_id}/schedulemanage_accountsBody interval_hours: 1 to 720, or null to stop scheduled scans.
GET /v1/onboardingview_findingsWhat the connect wizard needs.
GET /v1/onboarding/organizationsmanage_accountsThe StackSet template and the AWS Organizations member accounts that are not onboarded yet.
POST /v1/onboarding/policyview_findingsDrafts a read-only role from a plain-English description. Deterministic code checks the draft and swaps in the standard read-only role if it cannot prove the draft is read-only.

Scans#

RouteNeedsNotes
POST /v1/scansrun_scanBody account_id and optional regions. Leave out regions to scan every enabled region.
GET /v1/scansview_findingsQuery account_id (required) and limit (default 20, max 50).
GET /v1/scans/{scan_id}view_findingsStatus, progress and coverage.
GET /v1/scans/{scan_id}/reportview_findingsThe Risk Report.
GET /v1/scans/{scan_id}/diffview_findingsNew and resolved findings compared with the account's previous succeeded scan, computed on request.
GET /v1/scans/{scan_id}/briefview_findingsThe brief the worker stored when the scan succeeded: the same diff, plus verified fixes that regressed. Returns 404 when there is none.
GET /v1/scans/{scan_id}/patrolview_findingsWhat the autonomous patrol triaged and drafted.
GET /v1/scans/{scan_id}/attack-paths, GET /v1/scans/{scan_id}/attack-paths/diffview_findingsAttack paths, and how they changed.
GET /v1/scans/{scan_id}/graphview_findingsThe stored resource graph.
GET /v1/scans/{scan_id}/identitiesview_findingsNon-human identities, ranked with the most privileged and least used first.
GET /v1/scans/{scan_id}/least-privilegeview_findingsLeast-privilege policy drafts. kaveo never applies them.
GET /v1/scans/{scan_id}/data-inventoryview_findingsData stores, built from metadata only.
POST /v1/scans/{scan_id}/investigaterun_scanStarts an agentic investigation. Returns 409 unless KAVEO_AGENT_INVESTIGATION_ENABLED=true.
GET /v1/agent-runs/{run_id}, GET /v1/agent-runs/{run_id}/findingsview_findingsRun status, and the chains it found, with validated chains first.

Findings and evidence#

RouteNeedsNotes
GET /v1/findingsview_findingsQuery scan_id, severity, source (deterministic or native), include_suppressed, limit (default 50, max 500) and offset. severity matches one level exactly. Returns total and suppressed_count.
GET /v1/findings/exportview_findingsQuery format: ocsf (the default), asff, csv or json, plus the same filters. account_id and region are stamped onto ASFF output. Returns at most the 10,000 newest matches.
GET /v1/findings/{finding_id}view_findingsOne finding, with evidence_ids filled in.
GET /v1/findings/{finding_id}/observationsview_findingsThe observations the finding cites.
GET /v1/observations/{observation_id}view_findingsOne entry from the evidence ledger.

Grounded AI#

RouteNeedsNotes
POST /v1/findings/{finding_id}/prioritizeinvestigateAlso explain, remediate and compliance-impact at the same position in the path. Each stage also has a /stream variant.
POST /v1/investigateinvestigateStreams over SSE. Body finding_id, optional question (up to 2000 characters) and depth: fast, main (the default) or deep.
POST /v1/nlqueryinvestigateA question about a scan's graph. Body scan_id, question, and optional conversation_id to continue a conversation.
GET /v1/ai/configview_findingsThe active provider, where it came from, and the configured providers.
PUT /v1/ai/configmanage_accountsPins the provider at runtime, in this api process only. A hosted provider can be chosen only once its key is configured.

Remediation#

RouteNeedsNotes
POST /v1/findings/{finding_id}/remediations/proposeinvestigateProposes a remediation saga.
GET /v1/findings/{finding_id}/remediationsview_findingsThe finding's stored remediation artifacts.
POST /v1/findings/{finding_id}/remediate/sagainvestigateDrafts and verifies a grounded fix artifact for you to apply yourself.
GET /v1/remediationsview_findingsLists sagas.
GET /v1/remediations/operationsview_findingsThe rule ids that have an automated remediation operation.
GET /v1/remediations/{saga_id}view_findingsOne saga with its context.
POST /v1/remediations/{saga_id}/approveapprove_remediationAlso reject, execute and rollback. Only admins hold this permission.
POST /v1/remediations/{saga_id}/prapprove_remediationOpens the artifact as a GitHub pull request. Returns 409 until KAVEO_GITHUB_REPO and KAVEO_GITHUB_TOKEN are set.

Issues#

RouteNeedsNotes
POST /v1/scans/{scan_id}/issuesview_findingsBuilds or refreshes a scan's issues.
GET /v1/issuesview_findingsLists issues.
GET /v1/issues/{issue_id}view_findingsOne issue.
PATCH /v1/issues/{issue_id}triage_findingsSets any of status, assignee and note.
GET /v1/issues/{issue_id}/attack-pathsview_findingsAttack paths through the issue's resource.

Compliance#

RouteNeedsNotes
GET /v1/scans/{scan_id}/complianceview_findingsThe framework report for a scan.
GET /v1/scans/{scan_id}/compliance/exportview_findingsQuery format: csv (the default), json, evidence or pdf.
GET /v1/scans/{scan_id}/compliance/trendview_findingsThe account's score history per framework, oldest first.
GET /v1/compliance/rollupview_findingsThe latest succeeded scan of each account, per framework.
GET /v1/compliance/waivers, POST /v1/compliance/waiversapprove_remediationAccept-risk waivers.
DELETE /v1/compliance/waivers/{waiver_id}approve_remediationRevokes a waiver.

Sharing and email#

RouteNeedsNotes
GET /v1/scans/{scan_id}/shares, POST /v1/scans/{scan_id}/sharesshare_reportAnonymous, read-only report links.
DELETE /v1/shares/{share_id}share_reportRevokes a link.
GET /v1/shares/{token}/reportnoneThe shared report. Links expire after KAVEO_SHARE_TTL_DAYS (30 by default).
POST /v1/shares/{token}/emailnoneEmails a copy of the shared report, with a PDF and the same link, to the address in the body. No new link is minted. Returns 202.
POST /v1/reports/{scan_id}/emailview_findingsEmails the Risk Report with a PDF attached.

Integrations and MCP#

RouteNeedsNotes
GET /v1/alert-channels, POST /v1/alert-channelsmanage_accountsWebhook, Jira, Slack, EventBridge, SQS and S3 channels.
PATCH /v1/alert-channels/{channel_id}, DELETE /v1/alert-channels/{channel_id}manage_accountsUpdates or removes a channel.
GET /v1/notifications/configview_findingsWhich env-configured integrations are live. Booleans only, never secrets.
GET /v1/mcp/configview_findingsWhether the MCP server is on.
GET /v1/mcp/tokens, POST /v1/mcp/tokensview_findingsList or mint MCP tokens.
DELETE /v1/mcp/tokens/{token_id}view_findingsRevokes an MCP token.
POST /mcpMCP tokenJSON-RPC 2.0. Returns 403 unless KAVEO_MCP_ENABLED=true.

Organizations#

These routes need a signed-in caller. The handler then checks your role in that org. A platform superadmin passes the owner-or-admin checks, except when adding members.

RouteNeedsNotes
GET /v1/orgs, POST /v1/orgssigned inYour orgs, or create one.
PATCH /v1/orgs/{org_id}org owner or adminRenames the org.
DELETE /v1/orgs/{org_id}org ownerDeletes the org.
GET /v1/orgs/{org_id}/members, POST /v1/orgs/{org_id}/membersorg owner or adminLists or adds members.
PATCH /v1/orgs/{org_id}/members/{member_id}, DELETE /v1/orgs/{org_id}/members/{member_id}org owner or adminChanges or removes a member.
POST /v1/orgs/{org_id}/invitesorg owner or adminInvites a member.
POST /v1/orgs/{org_id}/transferorg owner or superadminTransfers ownership to another member.
GET /v1/orgs/{org_id}/planorg memberPlan, caps and current usage.
POST /v1/orgs/{org_id}/plansuperadminChanges the plan tier and caps.
POST /v1/orgs/{org_id}/erasemanage_users, plus org owner or superadminErases the org's data. This cannot be undone, and the body must set confirm to true.

Administration#

RouteNeedsNotes
GET /v1/admin/userssuperadminEvery user on the platform.
PATCH /v1/admin/users/{user_id}superadminChanges a user's base role, approval or superadmin flag. Only superadmins listed in KAVEO_SUPERADMIN_EMAILS can change the superadmin flag.
GET /v1/admin/orgssuperadminEvery org, with member and account counts.
POST /v1/admin/retentionmanage_accountsArchives scans older than older_than_days. The evidence ledger is never touched.
GET /v1/auditmanage_usersThe org's user-action audit trail, newest first.
GET /v1/system/workerview_findingsThe most recent worker heartbeat.

SCIM 2.0#

RouteNotes
GET /scim/v2/Users, POST /scim/v2/UsersList or provision users.
GET /scim/v2/Users/{user_id}Also PUT, PATCH and DELETE on the same path.
GET /scim/v2/Groups, GET /scim/v2/Groups/{group_id}Read-only groups.

SCIM accepts two bearer credentials:

  • The static token in KAVEO_SCIM_TOKEN. Users it creates are not placed in any org, and a user without an org gets a 403 on tenant routes. Treat this path as limited today.
  • A console session token of a signed-in user who holds manage_users. Users it creates join that user's org as members, with viewer access. A session expires after KAVEO_SESSION_TTL_HOURS, so the IdP's credential needs renewing. API tokens (kv_api_) are not accepted on SCIM.
text
handle /scim/* {
	reverse_proxy api:8000
}

Health and metrics#

RouteNotes
GET /healthNo auth. Returns status (ok or degraded), version, ai_provider, database, and the detector and collector counts.
GET /metricsNo auth. Prometheus text, including kaveo_detectors_loaded and kaveo_collectors_loaded.

Caddy does not forward either path to the api, so you can reach them only from inside the compose network. Keep it that way, or put them behind your own access control if you route them. See Deployment.

Streaming responses#

The /stream variants and POST /v1/investigate return text/event-stream. Each event is a single data: line holding a JSON object, and its type field says which event it is.

EventCarries
startFor a stage: kind and finding_id. For /v1/investigate: finding_id and depth.
claimOne grounded claim: text and citations, a list of observation ids. Only claims that pass the citation gate are sent.
contentThe stage's full output, plus persisted and ai_output_id.
doneNothing. The stream ends.

kaveo runs the stage, and stores the output when it cites evidence, before it sends the first byte. So a missing finding comes back as an ordinary 404, and a provider failure as a 503, never as a half-written stream.

Errors#

kaveo's errors use one envelope:

json
{"error": "forbidden", "detail": "account pending approval"}

A few 400 responses, such as an unknown provider sent to PUT /v1/ai/config, and requests to paths that do not exist use FastAPI's plain {"detail": "..."} shape instead. Read detail first and treat error as optional.

CodeHTTPNotes
unauthorized401The credential is missing, invalid or expired, or the login is locked out.
payment_required402The org's plan does not cover the action.
forbidden403You are authenticated but not permitted. Also returned for a pending account, a disabled MCP server and disabled signup.
not_found404Also returned for an unknown or unconfigured SSO provider, and for a revoked or expired share link.
conflict409The request conflicts with current state.
payload_too_large413The preferences body is over the size limit.
validation_error422Also includes errors, a list of objects with loc, msg and type.
rate_limited429Only when KAVEO_RATE_LIMIT_PER_MINUTE is above 0. The default is 0 (off), and you set it in a compose override. This response carries no X-Request-ID.
internal_error500An unexpected error. Quote the X-Request-ID when you report it.
github_unavailable502GitHub refused or could not be reached, and no pull request was opened.
ai_unavailable503A hosted AI provider still failed after its retries. The offline provider never returns this, and local falls back to the offline stub.
mfa_not_configured503Returned by POST /v1/auth/mfa/enroll while KAVEO_MFA_SECRET_KEY is not set. Compose does not pass it, so set a long random value in a compose override before users enroll.

The rate limiter counts requests per client address over a rolling 60 seconds, using the address the api sees. In the stock stack every request arrives through Caddy, so all clients share one budget. Size the limit with that in mind.