API reference

The whole surface,
on one page.

coudit speaks plain HTTP and JSON. Every response is a documented shape; every write is idempotent under the deduplication contract below.

Running instance required. The endpoints below are served by your own coudit backend — self-hosted, or the hosted SaaS once your organization is provisioned. Point $COUDIT_URL at it. A live OpenAPI/Swagger explorer is available at /docs on any running instance.

Authentication

Two mechanisms, same principal model. Machine callers (the CLI agent, CI) send a bearer token scoped to one organization. The dashboard uses an HttpOnly, signed session cookie. Every request resolves to a principal, and every product and finding is filtered to that principal’s organization — cross-tenant reads return 403.

# Tokens are shown exactly once at creation and stored only as a SHA-256 hash.
curl -H "Authorization: Bearer coudit_xxxxxxxxxxxxxxxxxxxx" \
     "$COUDIT_URL/api/v1/products"

The deduplication contract

Every incoming result is reduced to one signature:

signature = SHA-256( file_path | line_number | cwe | tool_name )

A signature is unique per product. When it already exists, coudit merges instead of inserting: hit_count increments, last_seen_at refreshes, the run is appended to merge_log, and the severity is upgraded if the new report is more severe. A finding previously marked resolved that reappears is reopened as active — a regression, not a new row. Re-running the same scan therefore never grows your backlog.

Severity normalization

Scanners disagree about severity, so coudit reconciles three sources in order: an explicit properties.severity, then properties.security-severity (CVSS 0–10), then the SARIF level.

CVSSSARIF levelNormalized
≥ 9.0Critical
7.0 – 8.9errorHigh
4.0 – 6.9warningMedium
0.1 – 3.9noteLow
0.0noneInfo

Endpoints

POST /api/v1/ingest

Accept a SARIF v2.1.0 bundle, normalize it, and deduplicate it into a product.

ParameterInDescription
productqueryProduct name, 1–255 chars. Created on first use, scoped to your org.
engagementqueryOpaque scan-run id, 1–64 chars. Groups results from one CI run.
bodyJSONSARIF bundle with a runs array. Capped at 50 MB.
curl -X POST "$COUDIT_URL/api/v1/ingest?product=api&engagement=$GITHUB_RUN_ID" \
  -H "Authorization: Bearer $COUDIT_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @bundle.sarif.json

# 200 OK
{
  "engagement_uid": "18273645",
  "product": "api",
  "new_findings": 7,
  "merged_findings": 12,
  "total_results_seen": 19,
  "tools_seen": ["opengrep", "trivy", "trufflehog"]
}

Malformed individual results are skipped and logged rather than failing the batch — one bad entry from a scanner never costs you the whole run.

GET /api/v1/products

List products visible to the caller, newest first.

[ { "id": 1, "name": "api", "description": null,
    "created_at": "2026-08-11T09:14:22Z" } ]

POST /api/v1/products

Create a product in the caller’s organization. Idempotent by name: posting an existing name returns the existing record rather than a duplicate or a conflict.

{ "name": "checkout-api", "description": "Payment service" }

GET /api/v1/products/{product_id}/findings

Active findings for one product, ordered by real severity (Critical → Info), then id. Optional ?severity= filter accepts one of Critical, High, Medium, Low, Info.

GET /api/v1/findings/{finding_id}

A single finding, including its cached AI remediation fields when generated.

{
  "id": 1,
  "tool_name": "opengrep",
  "rule_id": "python.lang.security.audit.formatted-sql-query",
  "title": "SQL injection in query builder",
  "severity": "Critical",
  "cwe": "CWE-89",
  "file_path": "app/db.py",
  "line_number": 42,
  "signature": "3c93affb...62ae442",
  "status": "active",
  "hit_count": 4,
  "first_seen_at": "2026-07-02T11:04:00Z",
  "last_seen_at": "2026-08-11T09:14:22Z",
  "ai_explanation": "...",
  "ai_secure_code_patch": "...",
  "ai_remediation_steps": ["..."]
}

GET /api/v1/export.csv

Stream every visible finding as CSV. Optional ?product_id= narrows the scope. Output is QUOTE_ALL with a UTF-8 BOM, and cells beginning =, +, -, @ or | are prefixed with an apostrophe so a hostile finding cannot become a live formula in Excel. Rows stream, so a million-row export never buffers in memory.

GET /api/v1/auth/tokens

List active tokens for your organization. Hashes are never returned.

POST /api/v1/auth/tokens

Mint a token. The plaintext field is present in this response and nowhere else, ever — store it immediately.

DELETE /api/v1/auth/tokens/{id}

Revoke a token. Takes effect on the next request.

GET /healthz

Unauthenticated liveness probe. Returns {"status":"ok"}.

Errors

StatusMeaning
400Malformed JSON, or not a SARIF bundle (no runs key).
401Missing or unrecognized credentials.
403Valid credentials, but the resource belongs to another organization.
404No such product or finding within your scope.
413Body exceeds the configured limit (50 MB by default).
500Internal error. Stack traces are never returned to callers.

Errors always carry a JSON body of the form {"detail": "..."}.

Need something that isn’t here? Webhook receivers for GitHub and GitLab, Slack notifications, and Stripe billing all ship in the same backend. Write to hello@coudit.com and we’ll point you at the right surface.