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.
| CVSS | SARIF level | Normalized |
|---|---|---|
| ≥ 9.0 | — | Critical |
| 7.0 – 8.9 | error | High |
| 4.0 – 6.9 | warning | Medium |
| 0.1 – 3.9 | note | Low |
| 0.0 | none | Info |
Endpoints
POST /api/v1/ingest
Accept a SARIF v2.1.0 bundle, normalize it, and deduplicate it into a product.
| Parameter | In | Description |
|---|---|---|
| product | query | Product name, 1–255 chars. Created on first use, scoped to your org. |
| engagement | query | Opaque scan-run id, 1–64 chars. Groups results from one CI run. |
| body | JSON | SARIF 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
| Status | Meaning |
|---|---|
| 400 | Malformed JSON, or not a SARIF bundle (no runs key). |
| 401 | Missing or unrecognized credentials. |
| 403 | Valid credentials, but the resource belongs to another organization. |
| 404 | No such product or finding within your scope. |
| 413 | Body exceeds the configured limit (50 MB by default). |
| 500 | Internal 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.