Skip to content

Endpoints: LLM Credentials, Billing & Findings

Store an LLM provider API key for the org, encrypted at rest. Used by /analyze as a fallback when the request doesn’t include an inline api_key. Upserts — storing again for the same provider replaces the previous key. Requires org-admin.

Request body

{"provider": "anthropic", "api_key": "sk-..."}

Response 201

{"provider": "anthropic", "created_at": "2026-07-09T00:00:00Z"}

List providers configured for the org. Never returns the key itself.

{"credentials": [{"provider": "anthropic", "created_at": "2026-07-09T00:00:00Z"}]}

DELETE /orgs/{org_id}/llm-credentials/{provider}

Section titled “DELETE /orgs/{org_id}/llm-credentials/{provider}”

Remove a stored credential. 404 if none exists for that provider.


Server-side analysis (/analyze) can be capped by an optional per-org monthly LLM spend budget. There is no billing-provider (Stripe, etc.) integration — this is internal metering plus a hard block. No cap is set by default (unlimited).

Set or clear the org’s monthly LLM spend cap. Requires org-admin.

Request body

{"monthly_budget_usd": 50.0}

Pass "monthly_budget_usd": null to clear the cap (back to unlimited).

Response 200

{"monthly_budget_usd": 50.0}

Current-month LLM usage and remaining budget for the org, plus its metered resource usage and — on an enterprise plan — where it stands against its commitment. Requires org-viewer.

Response 200

{
"period": "2026-07",
"total_cost_usd": 12.340000,
"budget_usd": 50.0,
"remaining_usd": 37.660000,
"event_count": 84,
"resource_count": 5400,
"metered_cost_usd": 2.4,
"included_resources_per_month": 5000,
"included_resources_remaining": 0,
"overage_resources": 400,
"committed_amount_usd": 1500.0,
"contract_expired": false,
"demo_analyses_used": null,
"demo_analyses_remaining": null
}

budget_usd and remaining_usd are null when the org has no cap set. period is the current calendar month in UTC (YYYY-MM); usage resets automatically at the start of each month — there’s no rollover.

resource_count is metered for every org regardless of plan — only billing is gated. What metered_cost_usd then means depends on the plan:

Planmetered_cost_usd
freenull — never billed
paidThe degressive per-resource price for the whole resource_count, from the first resource
enterpriseThe overage beyond included_resources_per_month, charged flat at the cheapest configured tier — 0.0, not null, while inside the commitment

The four commitment fields (included_resources_per_month, included_resources_remaining, overage_resources, committed_amount_usd) are null/0 on every plan but enterprise, and included_resources_per_month is also null for an enterprise org whose contract sets no ceiling — which means unlimited, not zero.

demo_analyses_used/demo_analyses_remaining are populated only on a cloud deployment for a non-paying plan — null on self-hosted or on any paying plan, where no demo quota applies.


A platform-wide (not per-org) catalog of which (llm_backend, model) pairs POST /{analyzer}/{namespace}/analyze and POST /orgs/{org_id}/integrations/github will accept, plus manually-entered pricing used to cost each analyze call. Superadmin-managed.

Any org member. Returns only enabled rows — this is what the dashboard’s Analyze and GitHub Integration forms fetch to populate their model picker.

Response 200

{
"models": [
{
"llm_backend": "anthropic",
"model": "claude-sonnet-5",
"input_price_per_1m_usd": 3.0,
"output_price_per_1m_usd": 15.0,
"enabled": true,
"created_by": "u_123",
"created_at": "2026-07-15T09:00:00+00:00",
"updated_at": null
}
]
}

Superadmin only. The full catalog, including disabled rows. Optional ?enabled=true|false filter.


Superadmin only. Adds a model to the catalog.

Request body — application/json

{
"llm_backend": "anthropic",
"model": "claude-sonnet-5",
"input_price_per_1m_usd": 3.0,
"output_price_per_1m_usd": 15.0,
"enabled": true
}
StatusDescription
201Created
409(llm_backend, model) already exists

Superadmin only. Partial update — send only the fields to change (typically input_price_per_1m_usd/output_price_per_1m_usd or enabled).


DELETE /admin/models/{llm_backend}/{model}

Section titled “DELETE /admin/models/{llm_backend}/{model}”

Superadmin only. Removes the model from the catalog. Any org still configured to use it (a saved GitHub integration, or a client that hardcodes it in /analyze calls) starts getting 400s the next time it’s used.


Every finding an analysis produces gets a persistent triage record, independent of the raw LLM output — keyed by (resource_hash, rule_id) within the caller’s org. There is no create endpoint: records appear automatically whenever an analysis is submitted (via either PUT /resource/{hash}/analysis or POST .../analyze). Re-analysing the same resource only refreshes last_seen/occurrence_count — it never resets a status you’ve already set.

List/filter finding records for the caller’s org. Requires member.

ParameterTypeDefaultDescription
statusstring—open, needs_review, acknowledged, suppressed, or false_positive
severitystring—e.g. CRITICAL, HIGH
rule_idstring—Exact rule ID
limitint100Page size (1–1000)
offsetint0Pagination offset

Response 200

{
"findings": [
{
"resource_hash": "a3f1c2d4e5b6",
"rule_id": "TFSEC-040",
"resource_address": "aws_db_instance.orders",
"severity": "HIGH",
"title": "S3 bucket allows public read",
"status": "open",
"first_seen": "2026-07-09T00:00:00Z",
"last_seen": "2026-07-09T00:00:00Z",
"occurrence_count": 1,
"note": null,
"updated_by": null,
"updated_at": null
}
]
}

Aggregated counts for the caller’s org. Requires member.

ParameterTypeDefaultDescription
daysint30Window for new_in_period/resolved_in_period (1–365)

Response 200

{
"period_days": 30,
"total": 42,
"by_status": {"open": 28, "needs_review": 2, "acknowledged": 5, "suppressed": 6, "false_positive": 1},
"by_severity_open": {"CRITICAL": 1, "HIGH": 10, "MEDIUM": 15, "LOW": 4},
"new_in_period": 8,
"resolved_in_period": 3
}

by_severity_open only counts findings currently status: "open". resolved_in_period counts findings whose status left open for anything other than needs_review within the window — a finding awaiting a human decision isn’t “resolved” just because it’s no longer open.

Set a finding’s triage status. Requires member — triage is treated as routine day-to-day work, not an admin action.

Request body

{"status": "suppressed", "reason_code": "accepted_risk", "note": "internal tool, not internet-facing"}

status is one of open, needs_review, acknowledged, suppressed, false_positive. Setting status: "open" reopens a previously triaged finding. note is optional.

reason_code is optional and one of false_positive, accepted_risk, fixed, not_applicable_here, other. Every status that carries a verdict (acknowledged, suppressed, false_positive) is appended to this org’s review-feedback log; the reason is what makes that entry usable. For suppressed it is the only thing separating “this is wrong” from “this is right and we accept it” — without it the verdict is recorded as unclear rather than guessed at, and counts toward neither side of the rule’s precision. Setting needs_review fires this org’s review webhook, if one is configured — the same notification an auto-flagged finding fires at creation time.

If the finding’s resource belongs to a namespace that was analysed for a GitHub pull request, this call also re-posts that PR’s commit status. A commit status is a push, not a poll — GitHub keeps whatever state was last written to a SHA — so triaging here is what clears a check held at pending, and it takes effect immediately rather than waiting for another push to re-trigger the webhook. One resource can gate several PRs, and every one of them is re-posted. This is best-effort: the status change is already saved when it runs, so a GitHub failure is logged and never surfaced in this response. See Clearing a held check.

Response 200 — the updated finding record (same shape as GET /findings)

StatusDescription
200Status updated
404No finding record exists for this (resource_hash, rule_id) pair

The append-only record of what humans decided about findings, and what it argues for. See Review Feedback Loop for the design, the guardrails, and why a pull-request comment is treated as attacker-controlled input.

List this org’s review feedback, newest first. Requires member.

Query parameters

ParameterDescription
rule_idOnly feedback on this rule
verdictconfirmed / rejected / severity_wrong / missed / unclear
sourcedashboard_triage / github_command / github_review_comment / api
resource_hashOnly feedback on this resource
limit / offsetPagination (default 100, max 1000)

Response 200

{
"feedback": [
{
"feedback_id": "…",
"resource_hash": "a3f1c2d4…",
"rule_id": "s3-public-acl",
"namespace": "gh-acme-infra-42",
"severity": "HIGH",
"verdict": "rejected",
"reason_code": "false_positive",
"comment": "data-lake buckets are public by design, tagged public-dataset",
"source": "dashboard_triage",
"actor": "usr_…",
"trusted": true,
"provenance": {"provider": null, "repo": null, "pr_number": null, "comment_id": null},
"created_at": "2026-08-24T09:12:00+00:00"
}
],
"total": 1
}

trusted is whether this entry may influence a later analysis — decided when it was written and never recomputed. Feedback from an outside contributor on a public pull request, or from a comment that tripped the injection scanner, is false: recorded and shown like any other, never composed into a prompt.

Record a verdict from a system this server doesn’t otherwise see — a code-review tool, a chat workflow, an internal triage queue. Requires member.

Triage through PATCH /findings/… records feedback by itself; this is not needed for it, and calling both counts one decision twice.

{"resource_hash": "a3f1c2d4…", "rule_id": "s3-public-acl", "verdict": "confirmed", "comment": "confirmed in the incident review"}
StatusDescription
201Recorded — returns the feedback document
404No finding exists for this (resource_hash, rule_id) pair

Per-rule reviewer precision. Requires member.

{
"rules": [
{
"rule_id": "s3-public-acl",
"title": "Public bucket ACL",
"severity": "HIGH",
"confirmed": 2, "rejected": 7, "severity_wrong": 0, "unclear": 3, "total": 12,
"precision": 0.222,
"calibrating": true,
"last_feedback_at": "2026-08-24T09:12:00+00:00"
}
],
"settings": {"calibration_enabled": true, "min_samples": 3, "severity_ceiling": "HIGH", "trusted_only": true}
}

precision is null until the rule has min_samples decisive verdicts — a ratio over one or two samples is noise. calibrating says whether the rule currently contributes to the analysis prompt; settings is echoed so a client can explain why a rule with feedback isn’t calibrating without hardcoding the deployment’s thresholds.

Rule changes the feedback argues for. Requires member. Filter with ?status=pending|applied|dismissed.

Each proposal carries the exact edit suggested (changes), the numbers behind it (evidence), and the reviewer comments it came from (reviewer_quotes).

Recompute proposals from current feedback. Requires admin. Idempotent — a rule with an open proposal has it updated rather than duplicated, and an open proposal the current evidence no longer supports is withdrawn. Deterministic and cheap (no LLM call), so calling it on a schedule is fine.

Accept a proposal. Requires admin. Edits the rule through the same path a hand-edited one takes, audited as rule.update plus a rule_proposal.apply event attributed to the caller. description_amendment, where present, is appended to the rule’s description rather than replacing it.

StatusDescription
200Applied — returns the decided proposal
404No such proposal, or the rule no longer exists
409Already applied or dismissed, or it carries no applicable change

POST /rule-proposals/{proposal_id}/dismiss

Section titled “POST /rule-proposals/{proposal_id}/dismiss”

Reject a proposal. Requires admin. The rule is untouched.


An org-level outbound notification fired whenever a finding enters needs_review — automatically (compute_initial_status, on low LLM confidence or a rule marked requires_review) or manually (PATCH /findings/{resource_hash}/{rule_id} above). Superseded by notification channels, which can reach Slack, allow more than one destination per org, and subscribe to events rather than being hardwired to this one. This endpoint still works and still fires — alongside channels, not instead of them — and there is nothing to migrate. Delivery is best-effort either way: a slow or failing endpoint never fails the request that triggered it, and there’s no retry queue.

Reuses the same (org_id, provider) shape as the GitHub integration (provider: "review_webhook") — one config per org, admin-only, same trust boundary as custom rules and LLM credentials.

PUT /orgs/{org_id}/integrations/review-webhook

Section titled “PUT /orgs/{org_id}/integrations/review-webhook”

Create or update. Requires admin role. Upserts — calling again replaces the URL/secret/enabled state entirely; omitting secret clears any previously configured signing secret rather than leaving the old one in place.

Request body

{"url": "https://example.com/hooks/opentremor", "secret": "whsec_...", "enabled": true}
FieldTypeDefaultDescription
urlstring—POSTed with a JSON body on every needs_review transition
secretstring | nullnullIf set, the request is signed with HMAC-SHA256 over the raw body and sent as X-OpenTremor-Signature: sha256=<hex> — the same scheme GitHub itself uses for inbound webhooks, just outbound here. Encrypted at rest, never returned by GET.
enabledbooltrue—

Response 200

{"url": "https://example.com/hooks/opentremor", "enabled": true, "has_secret": true, "created_at": "2026-01-01T00:00:00Z", "updated_at": null}

has_secret is the only signal GET gives about the secret — the value itself is never redisplayed after saving.

GET /orgs/{org_id}/integrations/review-webhook

Section titled “GET /orgs/{org_id}/integrations/review-webhook”

Current config. Requires admin role. 404 if none configured.

DELETE /orgs/{org_id}/integrations/review-webhook

Section titled “DELETE /orgs/{org_id}/integrations/review-webhook”

Remove it. Requires admin role. 404 if none configured.

Payload sent on delivery

{
"event": "finding.needs_review",
"org_id": "8f2a...",
"resource_hash": "a3f1c2d4e5b6",
"rule_id": "approved-module-only",
"resource_address": "aws_db_instance.orders",
"severity": "HIGH",
"title": "Direct Resource Instead Of Approved Module",
"namespaces": ["production-deploy-42"],
"trigger": "auto",
"timestamp": "2026-07-28T00:00:00Z"
}

trigger is "auto" (routed at analysis time) or "manual" (a human PATCHed the status directly).