Endpoints: LLM Credentials, Billing & Findings
LLM credentials
Section titled “LLM credentials”POST /orgs/{org_id}/llm-credentials
Section titled “POST /orgs/{org_id}/llm-credentials”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"}GET /orgs/{org_id}/llm-credentials
Section titled “GET /orgs/{org_id}/llm-credentials”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.
Billing / quota
Section titled “Billing / quota”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).
PATCH /orgs/{org_id}/quota
Section titled “PATCH /orgs/{org_id}/quota”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}GET /orgs/{org_id}/usage
Section titled “GET /orgs/{org_id}/usage”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:
| Plan | metered_cost_usd |
|---|---|
free | null — never billed |
paid | The degressive per-resource price for the whole resource_count, from the first resource |
enterprise | The 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.
Allowed models
Section titled “Allowed models”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.
GET /models
Section titled “GET /models”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 } ]}GET /admin/models
Section titled “GET /admin/models”Superadmin only. The full catalog, including disabled rows. Optional ?enabled=true|false filter.
POST /admin/models
Section titled “POST /admin/models”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}| Status | Description |
|---|---|
201 | Created |
409 | (llm_backend, model) already exists |
PATCH /admin/models/{llm_backend}/{model}
Section titled “PATCH /admin/models/{llm_backend}/{model}”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.
Findings
Section titled “Findings”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.
GET /findings
Section titled “GET /findings”List/filter finding records for the caller’s org. Requires member.
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | — | open, needs_review, acknowledged, suppressed, or false_positive |
severity | string | — | e.g. CRITICAL, HIGH |
rule_id | string | — | Exact rule ID |
limit | int | 100 | Page size (1–1000) |
offset | int | 0 | Pagination 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 } ]}GET /findings/summary
Section titled “GET /findings/summary”Aggregated counts for the caller’s org. Requires member.
| Parameter | Type | Default | Description |
|---|---|---|---|
days | int | 30 | Window 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.
PATCH /findings/{resource_hash}/{rule_id}
Section titled “PATCH /findings/{resource_hash}/{rule_id}”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", "note": "accepted risk — 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. Setting needs_review fires this org’s review webhook, if one is configured — the same notification an auto-flagged finding fires at creation time.
Response 200 — the updated finding record (same shape as GET /findings)
| Status | Description |
|---|---|
200 | Status updated |
404 | No finding record exists for this (resource_hash, rule_id) pair |
Review webhook
Section titled “Review webhook”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). This is the only outbound notification in this codebase — there’s no generic email/Slack system, and this isn’t one either; it’s scoped to exactly this one event. Delivery is best-effort: 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}| Field | Type | Default | Description |
|---|---|---|---|
url | string | — | POSTed with a JSON body on every needs_review transition |
secret | string | null | null | If 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. |
enabled | bool | true | — |
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).