HTTP API¶
The control plane (serve) exposes a REST API under /api, a Server-Sent Events stream at /api/sse, and the embedded web UI at /. All JSON endpoints return application/json.
Endpoint index¶
| Method | Path | Description |
|---|---|---|
POST |
/api/tasks |
Schedule a scan |
GET |
/api/tasks |
List tasks (?status=, ?show_seen=true) |
GET |
/api/tasks/{id} |
Get one task |
GET |
/api/tasks/{id}/result |
Get the analysis result for a completed task |
DELETE |
/api/tasks/{id} |
Delete a task |
POST |
/api/tasks/{id}/seen |
Mark a task seen |
POST |
/api/tasks/hide-all |
Hide all tasks |
GET |
/api/sse |
Server-Sent Events task status stream |
GET |
/api/workers |
List live workers |
POST |
/api/workers/{id}/pause |
Pause a worker |
POST |
/api/workers/{id}/unpause |
Unpause a worker |
POST |
/api/repos |
Register a repo (worker discovers its Go projects) |
POST |
/api/repos/analyze |
Schedule analysis (all projects or project) |
GET |
/api/repos/status?url= |
Registration status + discovered projects |
GET |
/api/repos |
Latest successful analysis per repo/project |
GET |
/api/repos/{id} |
One analysis by ID |
GET |
/api/repo-config?url=[&subdir=] |
Read per-project overrides |
POST |
/api/repo-config |
Set per-project overrides (partial upsert) |
DELETE |
/api/repo-config?url=[&subdir=] |
Remove per-project overrides |
GET |
/livez |
Liveness |
GET |
/readyz |
Readiness (Postgres + NATS) |
GET |
/health |
Alias of /livez |
GET |
/ |
Web UI (embedded single-page app) |
Tasks¶
POST /api/tasks — schedule a scan (legacy)¶
Kept for existing clients and the embedded UI. New clients should use POST /api/repos/analyze. It requires a registered repo (see POST /api/repos) and returns the first scheduled task only.
Request body:
{
"repo_url": "https://github.com/golang/example",
"ref": "HEAD",
"project": "tsc",
"engine": "local"
}
| Field | Required | Description |
|---|---|---|
repo_url |
yes | Git repository URL (must be registered) |
ref |
no | branch, tag, or commit SHA — persisted to the repo's root config |
project |
no | analyze only this project; omitted = all discovered projects |
engine |
no | defaults to worker_engine |
Response 201 Created:
{
"id": "task-1786482521535742990",
"repo_url": "https://github.com/golang/example",
"ref": "HEAD",
"status": "queued",
"engine": "local",
"attempt": 0,
"max_attempts": 3,
"created_at": "2026-08-11T12:00:00Z",
"updated_at": "2026-08-11T12:00:00Z"
}
GET /api/tasks — list tasks¶
Query params: status (pending|queued|running|completed|failed|expired), show_seen=true to include tasks marked seen.
GET /api/tasks/{id} — get a task¶
Returns the full task object. Once the task reaches completed, it carries analysis_id.
GET /api/tasks/{id}/result — get the analysis¶
Returns the stored analysis. Returns 404 if no analysis exists yet and 409 if the task has not completed.
{
"id": "a3f2c8d1e9b04f7a",
"repo_id": "b7c9d2e1f3a45678",
"repo_info": {
"url": "https://github.com/golang/example",
"ref": "HEAD",
"commit_sha": "abc123",
"metadata": { "stars": 12345, "forks": 3456, "language": "Go", "license": "BSD-2-Clause" }
},
"summary": { "average": 42.5, "combined": 85.0, "exceeded": 3, "total_funcs": 24 },
"entries": [ ],
"engine": "local",
"crap_version": "v0.5.1",
"analyzed_at": "2026-08-11T12:00:05Z",
"duration_ms": 4500,
"raw_output": { "version": "report-v1", "summary": { }, "entries": [ ] }
}
DELETE /api/tasks/{id} — 204 No Content¶
POST /api/tasks/{id}/seen — 204 No Content¶
POST /api/tasks/hide-all — 204 No Content¶
Server-Sent Events¶
GET /api/sse opens a long-lived text/event-stream. Each status change is pushed as a task event:
event: task
data: {"id":"task-...","repo_url":"https://github.com/golang/example","ref":"HEAD","status":"running","attempt":0,"max_attempts":3,"created_at":"2026-08-11T12:00:00Z","updated_at":"2026-08-11T12:00:01Z","seen":false,"hidden":false}
event: task
data: {"id":"task-...","repo_url":"https://github.com/golang/example","ref":"HEAD","status":"completed","attempt":1,"max_attempts":3,"created_at":"2026-08-11T12:00:00Z","updated_at":"2026-08-11T12:00:05Z","seen":false,"hidden":false}
Workers¶
| Endpoint | Description |
|---|---|
GET /api/workers |
List workers with id, host, pid, cpu, and status |
POST /api/workers/{id}/pause |
Stop a worker from consuming (204) |
POST /api/workers/{id}/unpause |
Resume a worker (204) |
Pausing one worker does not affect the others. See worker.
Repos¶
| Endpoint | Description |
|---|---|
POST /api/repos |
Register a repo — a worker clones it and discovers its Go projects |
POST /api/repos/analyze |
Schedule analysis (all projects, or one via project) |
GET /api/repos/status?url=<url> |
Registration status + discovered projects |
GET /api/repos |
Latest successful analysis per repo/project (excludes errors) |
GET /api/repos/{id} |
One analysis by ID |
POST /api/repos — register a repo¶
Request body: { "repo_url": "https://github.com/org/repo" }.
Response 202 Accepted: the repo record with Status: "registering". A worker clones the repo, discovers its Go projects, and updates the record to Status: "ready" (or "error" with StatusReason). Poll GET /api/repos/status until the status is no longer registering.
POST /api/repos/analyze — schedule analysis¶
Request body (repo_url required):
| Field | Required | Description |
|---|---|---|
repo_url |
yes | A registered repo URL |
project |
no | Analyze only this project (subdirectory); omitted = one task per discovered project |
Response 202 Accepted: the queued task(s).
GET /api/repos/status?url=<url> — registration status¶
Response: the repo record — Status (registering | ready | error), StatusReason, CommitSHA, ProfiledAt, and Profile.projects (discovered Go projects). 404 if the repo was never registered.
Per-project config¶
| Endpoint | Description |
|---|---|
GET /api/repo-config?url=<url>[&subdir=<project>] |
Read overrides |
POST /api/repo-config |
Partial upsert — only fields present in the body change |
DELETE /api/repo-config?url=<url>[&subdir=<project>] |
Remove overrides |
subdir scopes an override to one project of a multi-module repo; omit it (or send empty) for the repo root. A project's ref falls back to the repo-root config's ref when the project has none.
POST /api/repo-config body (all fields optional, url required):
{
"url": "https://github.com/org/repo",
"subdir": "tsc",
"ref": "release",
"test_args": "-race ./pkg/...",
"crap_args": ["--exclude", "legacy/.*"],
"submodules": true,
"timeout": "30m",
"git_timeout": "30m",
"go_flags": "-p=1 -parallel=2"
}
Health & readiness¶
| Endpoint | Purpose |
|---|---|
GET /livez |
Liveness — process is up |
GET /readyz |
Readiness — Postgres reachable and NATS connected |
GET /health |
Alias of /livez (used by the Docker HEALTHCHECK) |
curl http://localhost:8080/livez
# {"status":"ok"}
curl http://localhost:8080/readyz
# 200 {"status":"ok"} or 503 {"status":"error","reason":"..."}
The Kubernetes manifests in infra/ probe /readyz (readiness) and /livez (liveness); the Docker image uses /health in its HEALTHCHECK.
Errors¶
Errors return a JSON object with a message and the appropriate HTTP status (400 for bad input, 404 for missing resources, 500 for internal errors).